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 1c36e688ae92 CAMEL-26772: camel-chaos - what to try offers pairs of 
faults to try together (#29464)
1c36e688ae92 is described below

commit 1c36e688ae9283ae4693de9d91fccca74f0f5308
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 16:13:47 2026 +0100

    CAMEL-26772: camel-chaos - what to try offers pairs of faults to try 
together (#29464)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/chaos.adoc       |  23 ++++-
 components/camel-chaos/src/main/docs/chaos.adoc    |  23 ++++-
 .../apache/camel/component/chaos/ChaosAdvisor.java |  59 ++++++++++-
 .../apache/camel/component/chaos/ChaosManager.java |  10 +-
 .../camel/component/chaos/ChaosAdvisorTest.java    |   7 +-
 .../camel/component/chaos/ChaosPairsTest.java      | 114 +++++++++++++++++++++
 .../commands/action/CamelChaosSuggestAction.java   |  15 ++-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |   6 +-
 .../core/commands/action/CamelChaosActionTest.java |  20 ++++
 .../src/test/resources/golden/cli-help.txt         |   8 +-
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |   4 +-
 .../jbang/core/commands/tui/ChaosSuggestList.java  |   8 +-
 .../jbang/core/commands/tui/ChaosControlTest.java  |  19 ++++
 13 files changed, 292 insertions(+), 24 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 4d7c00bb6874..746bab5ac884 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
@@ -244,13 +244,30 @@ and not for the route that consumes it.
 message is lost between the two routes (see 
<<_expectations_on_time_and_loss,Expectations on time and loss>>)
 |An input from outside (not `direct` or `seda`) |The route fails at its entry 
(what does the sender see: a rollback, a
 retry, an error?), or the polls fail for a polling consumer; and the route 
stalls for a while
+|A remote call and a dead letter channel |Two faults at once: the call is down 
for a minute while the dead letter
+channel fails too. Are messages lost? The report counts what the dead letter 
channel did not take as lost
+|A remote call |Two faults at once: the call is slow and fails now and then. 
Do the redeliveries pile up, and how long
+does an exchange take?
 |===
 
+Two faults at once (kind `combined`) come after the single faults, as most 
bugs that lose messages need two failures
+that meet. Their rules are added together:
+
+[source]
+----
+5. route orders: An outage while the dead letter channel fails (combined)
+   route orders sends what fails to the dead letter channel direct:parked: 
when http://payment/charge is down for a minute and the dead letter channel 
fails too, are messages lost? The report counts what the dead letter channel 
did not take as lost
+   camel cmd chaos-add --rule='endpoint=http://payment/charge error status=503 
until=1m' --rule='endpoint=direct:parked error at=dlq until=1m'
+----
+
+Each of their rules counts as its own kind in what has been tried, so a pair 
is never "not tried yet".
+
 The rules leave the query of the endpoint out: its options do not tell which 
endpoint it is, and may hold a secret.
 `ChaosManager.suggest()` gives the same as JSON: `suggestions`, each with 
`route`, `kind`, `recipe`, `title`, `rule`,
-`why`, `tried`, `routes` for a dead letter channel that several routes share, 
and `feed`. A `feed` has its `kind`
-(one of `self`, `http`, `file` and `send`), a `hint` and `real`. The chaos 
screen of the Camel TUI lists the suggestions
-(key `w`), where Enter types the rule in to change or add.
+`why`, `tried`, `routes` for a dead letter channel that several routes share, 
`feed`, and `rules` for two faults at
+once. A `feed` has its `kind` (one of `self`, `http`, `file` and `send`), a 
`hint` and `real`. `rules` has all the
+rules to add together. The chaos screen of the Camel TUI lists the suggestions 
(key `w`), where Enter types the rule
+in to change or add (the rules of two faults at once separated by `;`).
 
 === What has been tried
 
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 4d7c00bb6874..746bab5ac884 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -244,13 +244,30 @@ and not for the route that consumes it.
 message is lost between the two routes (see 
<<_expectations_on_time_and_loss,Expectations on time and loss>>)
 |An input from outside (not `direct` or `seda`) |The route fails at its entry 
(what does the sender see: a rollback, a
 retry, an error?), or the polls fail for a polling consumer; and the route 
stalls for a while
+|A remote call and a dead letter channel |Two faults at once: the call is down 
for a minute while the dead letter
+channel fails too. Are messages lost? The report counts what the dead letter 
channel did not take as lost
+|A remote call |Two faults at once: the call is slow and fails now and then. 
Do the redeliveries pile up, and how long
+does an exchange take?
 |===
 
+Two faults at once (kind `combined`) come after the single faults, as most 
bugs that lose messages need two failures
+that meet. Their rules are added together:
+
+[source]
+----
+5. route orders: An outage while the dead letter channel fails (combined)
+   route orders sends what fails to the dead letter channel direct:parked: 
when http://payment/charge is down for a minute and the dead letter channel 
fails too, are messages lost? The report counts what the dead letter channel 
did not take as lost
+   camel cmd chaos-add --rule='endpoint=http://payment/charge error status=503 
until=1m' --rule='endpoint=direct:parked error at=dlq until=1m'
+----
+
+Each of their rules counts as its own kind in what has been tried, so a pair 
is never "not tried yet".
+
 The rules leave the query of the endpoint out: its options do not tell which 
endpoint it is, and may hold a secret.
 `ChaosManager.suggest()` gives the same as JSON: `suggestions`, each with 
`route`, `kind`, `recipe`, `title`, `rule`,
-`why`, `tried`, `routes` for a dead letter channel that several routes share, 
and `feed`. A `feed` has its `kind`
-(one of `self`, `http`, `file` and `send`), a `hint` and `real`. The chaos 
screen of the Camel TUI lists the suggestions
-(key `w`), where Enter types the rule in to change or add.
+`why`, `tried`, `routes` for a dead letter channel that several routes share, 
`feed`, and `rules` for two faults at
+once. A `feed` has its `kind` (one of `self`, `http`, `file` and `send`), a 
`hint` and `real`. `rules` has all the
+rules to add together. The chaos screen of the Camel TUI lists the suggestions 
(key `w`), where Enter types the rule
+in to change or add (the rules of two faults at once separated by `;`).
 
 === What has been tried
 
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
index 2d44cbb71ecf..5c88f23aca89 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
@@ -77,7 +77,9 @@ final class ChaosAdvisor {
         DUPLICATE("duplicate"),
         CONSUMER("consumer"),
         LIFECYCLE("lifecycle"),
-        DEAD_LETTER("dead letter channel");
+        DEAD_LETTER("dead letter channel"),
+        // two faults at once (CAMEL-26772): its rules are tried as their own 
kinds
+        COMBINED("combined");
 
         final String key;
 
@@ -98,12 +100,29 @@ final class ChaosAdvisor {
      * @param rank   higher is tried first
      * @param routes the routes a rule of its kind tries it in: the route, and 
the other routes of a dead letter channel
      *               they share, which is suggested once
+     * @param with   the other rules to add with the rule, for two faults at 
once (CAMEL-26772); empty for one fault
      */
     record Suggestion(String route, Kind kind, String recipe, String title, 
String rule, String why, int rank,
-            List<String> routes) {
+            List<String> routes, List<String> with) {
 
         Suggestion(String route, Kind kind, String recipe, String title, 
String rule, String why, int rank) {
-            this(route, kind, recipe, title, rule, why, rank, List.of(route));
+            this(route, kind, recipe, title, rule, why, rank, List.of(route), 
List.of());
+        }
+
+        Suggestion(String route, Kind kind, String recipe, String title, 
String rule, String why, int rank,
+                   List<String> routes) {
+            this(route, kind, recipe, title, rule, why, rank, routes, 
List.of());
+        }
+
+        /** The rules to add: the rule, and the rules to add with it. */
+        List<String> rules() {
+            if (with.isEmpty()) {
+                return List.of(rule);
+            }
+            List<String> answer = new ArrayList<>();
+            answer.add(rule);
+            answer.addAll(with);
+            return List.copyOf(answer);
         }
     }
 
@@ -230,6 +249,10 @@ final class ChaosAdvisor {
     static Map<String, Map<Kind, List<String>>> 
suggestedKinds(List<Suggestion> suggestions) {
         Map<String, Map<Kind, List<String>>> answer = new LinkedHashMap<>();
         for (Suggestion s : suggestions) {
+            if (s.kind() == Kind.COMBINED) {
+                // its rules are tried as their own kinds, which the single 
suggestions offer
+                continue;
+            }
             answer.computeIfAbsent(s.route(), k -> new 
LinkedHashMap<>()).putIfAbsent(s.kind(), s.routes());
         }
         return answer;
@@ -368,6 +391,9 @@ final class ChaosAdvisor {
 
         // the sends: each endpoint once
         Set<String> seen = new LinkedHashSet<>();
+        // the first call to a remote system (not a local file), for the 
faults to try two at a time
+        String call = null;
+        boolean httpCall = false;
         for (Processor processor : route.filter("*")) {
             if (!(processor instanceof EndpointAware aware) || 
aware.getEndpoint() == null) {
                 continue;
@@ -393,6 +419,10 @@ final class ChaosAdvisor {
             if (!ChaosStubs.isRemote(to)) {
                 continue;
             }
+            if (call == null && !LOCAL_FILES.contains(scheme)) {
+                call = uri;
+                httpCall = ChaosRuleParser.HTTP_SCHEMES.contains(scheme);
+            }
             if (LOCAL_FILES.contains(scheme)) {
                 // a local file system: nothing connects, but the write can 
fail
                 add(answer, recipes, "write-error", id, Kind.ERRORS, 
Map.of("endpoint", uri),
@@ -436,6 +466,29 @@ final class ChaosAdvisor {
                     78, shared);
         }
 
+        // two faults at once, after the single ones: most bugs that lose 
messages need two failures that meet
+        if (call != null) {
+            String fails = httpCall ? "error status=503" : "error 
exception=java.net.ConnectException";
+            if (dead != null) {
+                answer.add(new Suggestion(
+                        id, Kind.COMBINED, null, "An outage while the dead 
letter channel fails",
+                        "endpoint=" + call + " " + fails + " until=1m", "route 
" + id + " sends what fails to the dead"
+                                                                        + " 
letter channel " + dead + ": when " + call
+                                                                        + " is 
down for a minute and the dead letter channel"
+                                                                        + " 
fails too, are messages lost? The report counts"
+                                                                        + " 
what the dead letter channel did not take as lost",
+                        52, List.of(id), List.of("endpoint=" + dead + " error 
at=dlq until=1m")));
+            }
+            answer.add(new Suggestion(
+                    id, Kind.COMBINED, null, "A slow service that also fails",
+                    "endpoint=" + call + " delay duration=1s..3s rate=0.5 
until=2m",
+                    "a call to " + call + " that is slow and fails now and 
then: do the redeliveries pile up, and how"
+                                                                               
      + " long does an exchange take? An expectation such as expect="
+                                                                               
      + id
+                                                                               
      + ":maxTime<=30s says how long is too long",
+                    45, List.of(id), List.of("endpoint=" + call + " " + fails 
+ " rate=0.2 until=2m")));
+        }
+
         // the input of the route
         if (from != null && ChaosStubs.isRemote(from)) {
             if (route.getConsumer() instanceof ScheduledPollConsumer) {
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 55ca97db689a..5c88b3888480 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
@@ -511,8 +511,10 @@ public final class ChaosManager {
         Map<String, ChaosAdvisor.Feed> feeds = new HashMap<>();
         for (ChaosAdvisor.Suggestion s : ChaosAdvisor.suggest(camelContext)) {
             JsonObject jo = new JsonObject();
-            // a dead letter channel that routes share is tried by a rule of 
its kind in any of them
-            if (s.routes().stream().anyMatch(r -> tried.contains(r + " " + 
s.kind().key))) {
+            // a dead letter channel that routes share is tried by a rule of 
its kind in any of them; two faults at once
+            // are tried as their own kinds
+            if (s.kind() != ChaosAdvisor.Kind.COMBINED
+                    && s.routes().stream().anyMatch(r -> tried.contains(r + " 
" + s.kind().key))) {
                 jo.put("tried", true);
             }
             jo.put("route", s.route());
@@ -525,6 +527,10 @@ public final class ChaosManager {
             }
             jo.put("title", s.title());
             jo.put("rule", s.rule());
+            if (!s.with().isEmpty()) {
+                // two faults at once: all the rules to add together
+                jo.put("rules", new JsonArray(s.rules()));
+            }
             jo.put("why", s.why());
             Route route = camelContext.getRoute(s.route());
             ChaosAdvisor.Feed feed = route != null ? 
feeds.computeIfAbsent(s.route(), k -> ChaosAdvisor.feed(route)) : null;
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
index 934e49c1882d..14a6fd12526c 100644
--- 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosAdvisorTest.java
@@ -104,8 +104,11 @@ class ChaosAdvisorTest extends ChaosTestSupport {
     @Test
     void everySuggestedRuleIsValid() {
         for (ChaosAdvisor.Suggestion s : ChaosAdvisor.suggest(context)) {
-            String id = chaos().addRule(s.rule());
-            chaos().removeRule(id);
+            // two faults at once too: each of their rules
+            for (String rule : s.rules()) {
+                String id = chaos().addRule(rule);
+                chaos().removeRule(id);
+            }
         }
     }
 
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosPairsTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosPairsTest.java
new file mode 100644
index 000000000000..537276989892
--- /dev/null
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosPairsTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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 org.apache.camel.BindToRegistry;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * What to try offers two faults at once (CAMEL-26772): an outage while the 
dead letter channel fails, and a slow
+ * service that also fails, after the single faults.
+ */
+class ChaosPairsTest extends ChaosTestSupport {
+
+    @BindToRegistry("http")
+    private final TestHttpComponent http = new TestHttpComponent();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:orders").routeId("orders")
+                        
.errorHandler(deadLetterChannel("seda:parked").maximumRedeliveries(2))
+                        .to("http://payment/charge";)
+                        .to("mock:shipped");
+                
from("seda:parked?pollTimeout=10").routeId("parked").to("mock:parked");
+                // a route that calls nothing remote has no pairs
+                from("direct:audit").routeId("audit").to("mock:audit");
+            }
+        };
+    }
+
+    private ChaosAdvisor.Suggestion pair(String title) {
+        return ChaosAdvisor.suggest(context).stream().filter(s -> 
title.equals(s.title())).findFirst().orElseThrow();
+    }
+
+    @Test
+    void anOutageWhileTheDeadLetterChannelFails() {
+        ChaosAdvisor.Suggestion s = pair("An outage while the dead letter 
channel fails");
+        assertThat(s.route()).isEqualTo("orders");
+        assertThat(s.kind()).isEqualTo(ChaosAdvisor.Kind.COMBINED);
+        assertThat(s.rules()).containsExactly(
+                "endpoint=http://payment/charge error status=503 until=1m",
+                "endpoint=seda:parked error at=dlq until=1m");
+        assertThat(s.why()).contains("dead letter channel seda:parked", "are 
messages lost?");
+    }
+
+    @Test
+    void aSlowServiceThatAlsoFails() {
+        ChaosAdvisor.Suggestion s = pair("A slow service that also fails");
+        assertThat(s.rules()).containsExactly(
+                "endpoint=http://payment/charge delay duration=1s..3s rate=0.5 
until=2m",
+                "endpoint=http://payment/charge error status=503 rate=0.2 
until=2m");
+        assertThat(s.why()).contains("expect=orders:maxTime<=30s");
+    }
+
+    @Test
+    void afterTheSingleFaults() {
+        List<ChaosAdvisor.Suggestion> suggestions = 
ChaosAdvisor.suggest(context);
+        int firstPair = suggestions.indexOf(pair("An outage while the dead 
letter channel fails"));
+        // the HTTP error and the dead letter channel on their own come first
+        assertThat(suggestions.subList(0, 
firstPair)).extracting(ChaosAdvisor.Suggestion::recipe)
+                .contains("http-error", "dlq-unreachable");
+        assertThat(suggestions).noneMatch(s -> "audit".equals(s.route()));
+    }
+
+    @Test
+    void theJsonHasAllTheRulesAndNoTriedForAPair() {
+        // the rules of the pair fired on their own, as their own kinds
+        chaos().addRule("endpoint=http://payment/charge error status=503 
until=1");
+        template.sendBody("direct:orders", "order");
+
+        JsonArray list = (JsonArray) chaos().suggest().get("suggestions");
+        JsonObject pair = list.stream().map(JsonObject.class::cast)
+                .filter(s -> "An outage while the dead letter channel 
fails".equals(s.getString("title")))
+                .findFirst().orElseThrow();
+        assertThat(pair.getString("kind")).isEqualTo("combined");
+        
assertThat(pair.getString("rule")).isEqualTo("endpoint=http://payment/charge 
error status=503 until=1m");
+        List<Object> rules = pair.getCollection("rules");
+        assertThat(rules).containsExactly(
+                "endpoint=http://payment/charge error status=503 until=1m",
+                "endpoint=seda:parked error at=dlq until=1m");
+        assertThat(pair.containsKey("tried")).isFalse();
+        // a single suggestion has no rules list
+        JsonObject single = list.stream().map(JsonObject.class::cast)
+                .filter(s -> 
"http-error".equals(s.getString("recipe"))).findFirst().orElseThrow();
+        assertThat(single.containsKey("rules")).isFalse();
+        assertThat(single.getBoolean("tried")).isTrue();
+
+        // and combined is never "not tried yet": its rules are tried as their 
own kinds
+        assertThat(chaos().report().toText()).doesNotContain("combined");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
index 551b173148c6..e446080ecc1f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosSuggestAction.java
@@ -34,9 +34,11 @@ import picocli.CommandLine;
                              "(Kafka, JMS: the call is made twice), a dead 
letter channel (it fails too), a",
                              "hand-off to another route over seda (the next 
route fails, and no message may",
                              "be lost between the two), an input from outside 
(the route fails at its entry,",
-                             "the polls fail, the route stalls). The most 
worth trying come first, each with",
-                             "why and the command that adds it, and how to 
feed the route with messages",
-                             "(curl, a file, or camel cmd send), unless it 
feeds itself. Nothing is added.",
+                             "the polls fail, the route stalls), and then two 
faults at once (an outage while",
+                             "the dead letter channel fails, a slow service 
that also fails). The most worth",
+                             "trying come first, each with why, the command 
that adds it, and how to feed the",
+                             "route with messages (curl, a file, or camel cmd 
send), unless it feeds itself.",
+                             "Nothing is added.",
                              "%nWithout a name or pid, the only integration 
that runs, or the only one with",
                              "chaos switched on.",
                              "%nExamples:",
@@ -83,7 +85,12 @@ public class CamelChaosSuggestAction extends 
CamelChaosAction {
                 sb.append(" - tried");
             }
             sb.append("\n   ").append(s.getString("why"));
-            sb.append("\n   camel cmd chaos-add 
--rule='").append(s.getString("rule")).append('\'');
+            // two faults at once are added together
+            List<Object> rules = s.getCollection("rules");
+            sb.append("\n   camel cmd chaos-add");
+            for (Object rule : rules != null && !rules.isEmpty() ? rules : 
List.of(s.getString("rule"))) {
+                sb.append(" --rule='").append(rule).append('\'');
+            }
             JsonObject feed = s.getMap("feed");
             if (feed != null && !"self".equals(feed.getString("kind"))) {
                 // how to send messages into the route, so the rule has 
something to act on
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 476dbf4030cc..16c490f78347 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
@@ -844,8 +844,10 @@ public final class ToolRegistry {
                                        + "route, the kind of fault, the rule, 
why to try it, and tried=true when a rule "
                                        + "of that kind has already faulted the 
route, and feed: how to send messages "
                                        + "into the route (kind self, http, 
file or send, a hint such as the command, and "
-                                       + "real=true when that goes to the real 
system rather than a stub). Nothing is "
-                                       + "added: check a rule with chaos_check 
and add it with chaos_add.")
+                                       + "real=true when that goes to the real 
system rather than a stub). Two faults "
+                                       + "at once (kind combined, after the 
single ones) have rules: all the rules to "
+                                       + "add together (chaos_add with them 
separated by ;). Nothing is added: check a "
+                                       + "rule with chaos_check and add it 
with chaos_add.")
                 .param("name", "string", CHAOS_NAME, false)
                 .readOnly(true).destructive(false)
                 .executor((ctx, args) -> {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
index 34d773dd25b7..dfab28212f77 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelChaosActionTest.java
@@ -170,6 +170,26 @@ class CamelChaosActionTest extends 
ActionCommandTestSupport {
         assertThat(out.split("real system", -1)).hasSize(2);
     }
 
+    @Test
+    void suggestAddsTwoFaultsAtOnceTogether() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        JsonObject pair = new JsonObject(
+                Map.of("route", "orders", "kind", "combined", "title", "An 
outage while the dead letter channel fails",
+                        "rule", "endpoint=http://pay error status=503 
until=1m",
+                        "rules", new JsonArray(
+                                List.of("endpoint=http://pay error status=503 
until=1m",
+                                        "endpoint=seda:dlq error at=dlq 
until=1m")),
+                        "why", "route orders sends what fails to the dead 
letter channel seda:dlq"));
+        JsonObject answer = new JsonObject(Map.of("suggestions", new 
JsonArray(List.of(pair))));
+
+        assertThat(callWithResponse(command(new 
CamelChaosSuggestAction(main()), "orders"), answer)).isZero();
+
+        assertThat(printer.getLines()).contains(
+                "1. route orders: An outage while the dead letter channel 
fails (combined)",
+                "camel cmd chaos-add --rule='endpoint=http://pay error 
status=503 until=1m'"
+                                                                               
              + " --rule='endpoint=seda:dlq error at=dlq until=1m'");
+    }
+
     @Test
     void suggestSaysWhenThereIsNothingToTry() throws Exception {
         writeStatusFile(TEST_PID, "orders");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index dd3453c3640e..a4b2b6dd9b2d 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -810,9 +810,11 @@ breaker (the service is down for a while), a call that the 
input may repeat
 (Kafka, JMS: the call is made twice), a dead letter channel (it fails too), a
 hand-off to another route over seda (the next route fails, and no message may
 be lost between the two), an input from outside (the route fails at its entry,
-the polls fail, the route stalls). The most worth trying come first, each with
-why and the command that adds it, and how to feed the route with messages
-(curl, a file, or camel cmd send), unless it feeds itself. Nothing is added.
+the polls fail, the route stalls), and then two faults at once (an outage while
+the dead letter channel fails, a slow service that also fails). The most worth
+trying come first, each with why, the command that adds it, and how to feed the
+route with messages (curl, a file, or camel cmd send), unless it feeds itself.
+Nothing is added.
 
 Without a name or pid, the only integration that runs, or the only one with
 chaos switched on.
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 1a13c17f93ed..1f7f8e290b26 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -1174,7 +1174,9 @@ public class RuntimeTools {
                   breaker, a dead letter channel or a hand-off to another 
route over seda. Each has the route, the \
                   kind of fault, the rule, why to try it, tried=true when a 
rule of that kind has already faulted \
                   the route, and feed: how to send messages into the route 
(kind self, http, file or send, a hint \
-                  such as the command, and real=true when that goes to the 
real system rather than a stub). \
+                  such as the command, and real=true when that goes to the 
real system rather than a stub). Two \
+                  faults at once (kind combined, after the single ones) have 
rules: all the rules to add together \
+                  (camel_runtime_chaos_add with them separated by ;). \
                   Nothing is added: check a rule with 
camel_runtime_chaos_check and add it with \
                   camel_runtime_chaos_add.""")
     public JsonObject camel_runtime_chaos_suggest(
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
index 6fd8e25102f1..c338c44b37ab 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosSuggestList.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 import dev.tamboui.layout.Constraint;
 import dev.tamboui.layout.Layout;
@@ -71,9 +72,14 @@ final class ChaosSuggestList {
         if (reply != null && reply.getCollection("suggestions") != null) {
             for (Object o : reply.getCollection("suggestions")) {
                 if (o instanceof JsonObject jo && jo.getString("rule") != 
null) {
+                    // two faults at once (CAMEL-26772): the rules are added 
together, separated by ;
+                    List<Object> rules = jo.getCollection("rules");
+                    String rule = rules != null && !rules.isEmpty()
+                            ? 
rules.stream().map(String::valueOf).collect(Collectors.joining("; "))
+                            : jo.getString("rule");
                     items.add(new Item(
                             jo.getString("route"), jo.getString("kind"), 
jo.getString("title"),
-                            jo.getString("rule"), jo.getString("why"), 
jo.getBooleanOrDefault("tried", false)));
+                            rule, jo.getString("why"), 
jo.getBooleanOrDefault("tried", false)));
                 }
             }
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
index 549ad0b6108f..b5209809c32b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControlTest.java
@@ -255,6 +255,25 @@ class ChaosControlTest {
         assertThat(requests).hasSize(1);
     }
 
+    @Test
+    void twoFaultsAtOnceAreTypedInTogether() throws Exception {
+        reply = (JsonObject) Jsoner.deserialize("""
+                {"suggestions": [
+                  {"route": "orders", "kind": "combined", "title": "An outage 
while the dead letter channel fails",
+                   "rule": "endpoint=http://pay error status=503 until=1m",
+                   "rules": ["endpoint=http://pay error status=503 until=1m", 
"endpoint=seda:dlq error at=dlq until=1m"],
+                   "why": "route orders sends what fails to the dead letter 
channel seda:dlq"}]}
+                """);
+        key('w');
+        enter();
+        assertThat(tab.isTextInputActive()).isTrue();
+        // added after Enter and the confirmation, both rules in one add
+        enter();
+        key('y');
+        assertThat(requests.get(requests.size() - 1).getString("rule"))
+                .isEqualTo("endpoint=http://pay error status=503 until=1m; 
endpoint=seda:dlq error at=dlq until=1m");
+    }
+
     @Test
     void whatToTryWithNothingToSuggest() throws Exception {
         reply = (JsonObject) Jsoner.deserialize("{\"suggestions\": []}");

Reply via email to