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

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

commit f3320abb6899fdf1a5783a5d06521ae5ff47ef36
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 17:56:36 2026 +0100

    chore: camel-jbang - let there be chaos: polish from trying it on more 
examples
    
    The picks take a kind of fault once before it is tried again in another 
route (message-size has three hand-offs of
    one kind), L says why when chaos is off or not known yet, and the dialog 
says what to do when every route gets its
    messages from outside (http-client).
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |  7 ++--
 .../dsl/jbang/core/commands/tui/ChaosControl.java  | 12 +++++-
 .../core/commands/tui/ChaosExploreDialog.java      | 48 +++++++++++++++++-----
 .../dsl/jbang/core/commands/tui/ChaosTab.java      |  2 +
 .../src/main/resources/tui/help/chaos.md           |  8 ++--
 .../jbang/core/commands/tui/ChaosExploreTest.java  | 29 +++++++++++++
 6 files changed, 89 insertions(+), 17 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index eb9fb55ead48..8ca57af15b05 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -395,9 +395,10 @@ that never fired, or a route that had no messages). *l* or 
Esc shows the rules a
 
 Press *L* to start an exploration by hand, on the integration you look at: 
pick the rules to try from what the
 integration suggests, the most worth trying first, each with what should hold 
despite its faults, and a budget of 2 to
-30 minutes. The single faults not tried yet are picked, of the routes that get 
messages by themselves (a timer, or a
-route they are called from); a route whose messages come from outside is 
marked, as its rule says nothing without
-messages. *Space* picks a rule, *Left* and *Right* change the budget, and 
*Enter* starts. The picked rules are tried one
+30 minutes. Picked are a few faults not tried yet, each kind once before it is 
tried again in another route, and then
+two faults at once, such as an outage while the dead letter channel fails; 
only of the routes that get messages by
+themselves (a timer, or a route they are called from). A route whose messages 
come from outside is marked, as its rule
+says nothing without messages. *Space* picks a rule, *Left* and *Right* change 
the budget, and *Enter* starts. The picked rules are tried one
 at a time, each for its share of the budget, and each ends with PASS, FAIL or 
INCONCLUSIVE, while the line above the
 experiments says which rule is tried. *x* stops it, which removes the rules at 
once. Every rule is removed when the
 budget ends, also when the TUI is closed. For a longer exploration, where an 
AI picks what to try next, use
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
index 45442bd1b7de..93d290500f57 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosControl.java
@@ -358,7 +358,17 @@ final class ChaosControl {
      * suggests to pick from, or says why it cannot start.
      */
     void openExplore(IntegrationInfo info) {
-        if (info == null || info.chaos == null || !info.chaos.enabled) {
+        if (info == null) {
+            return;
+        }
+        if (info.chaos == null || !info.chaos.enabled) {
+            // the key says why it does nothing: chaos is off, or the status 
of the integration has not said yet
+            name = info.name;
+            rule = null;
+            command = Command.ADD;
+            result("Let there be chaos", true, info.chaos == null
+                    ? "camel-chaos is not in " + info.name + " (yet): run it 
with camel run --chaos"
+                    : info.chaos.reason != null ? info.chaos.reason : "Chaos 
is off in " + info.name);
             return;
         }
         pid = info.pid;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java
index 6098e30fd15d..65c21a00d316 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreDialog.java
@@ -18,7 +18,9 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.ArrayList;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -113,16 +115,39 @@ final class ChaosExploreDialog {
         // the faults not tried yet whose routes get messages by themselves, 
the most worth trying first: a few single
         // ones, then two at once, as most bugs that lose messages need two 
failures that meet; a route without messages
         // says nothing, and a dead letter channel fails only when another 
fault sends it a message (the pair does)
+        items.forEach(item -> picked.add(false));
+        // a kind of fault once, before one is tried again in another route: 
the budget asks more questions so
+        pick(true);
+        pick(false);
+    }
+
+    private void pick(boolean distinctKinds) {
+        Set<String> kinds = new HashSet<>();
         int singles = 0;
         int combined = 0;
-        for (Item item : items) {
-            boolean worth = !item.tried() && item.fed() && item.expect() != 
null && !item.deadLetter();
-            boolean pick = worth && (item.combined() ? combined < 
PICKED_COMBINED : singles < PICKED_SINGLE);
-            picked.add(pick);
-            if (pick && item.combined()) {
-                combined++;
-            } else if (pick) {
-                singles++;
+        for (int i = 0; i < items.size(); i++) {
+            Item item = items.get(i);
+            if (picked.get(i)) {
+                kinds.add(item.kind());
+                if (item.combined()) {
+                    combined++;
+                } else {
+                    singles++;
+                }
+            }
+        }
+        for (int i = 0; i < items.size(); i++) {
+            Item item = items.get(i);
+            boolean worth = !picked.get(i) && !item.tried() && item.fed() && 
item.expect() != null && !item.deadLetter()
+                    && (!distinctKinds || !kinds.contains(item.kind()));
+            if (worth && (item.combined() ? combined < PICKED_COMBINED : 
singles < PICKED_SINGLE)) {
+                picked.set(i, true);
+                kinds.add(item.kind());
+                if (item.combined()) {
+                    combined++;
+                } else {
+                    singles++;
+                }
             }
         }
     }
@@ -159,7 +184,10 @@ final class ChaosExploreDialog {
     /** Why the exploration cannot start, or null when it can. */
     String cannotStart() {
         if (picked().isEmpty()) {
-            return "Pick a rule to try (Space)";
+            return !items.isEmpty() && items.stream().noneMatch(Item::fed)
+                    ? "The routes here get their messages from outside: pick a 
rule (Space), and send its route messages"
+                      + " while it runs, or its verdict says nothing"
+                    : "Pick a rule to try (Space)";
         }
         if (share() < MIN_SHARE) {
             return "Too many rules for " + budget() + ": pick fewer, or a 
longer budget";
@@ -189,7 +217,7 @@ final class ChaosExploreDialog {
 
     void render(Frame frame, Rect area) {
         int width = Math.min(area.width() - 4, 150);
-        int height = Math.min(area.height() - 2, Math.max(16, items.size() + 
10));
+        int height = Math.min(area.height() - 2, items.isEmpty() ? 5 : 
Math.max(16, items.size() + 10));
         Rect popup = DialogHelper.centered(area, width, height);
         frame.renderWidget(Clear.INSTANCE, popup);
         Block outer = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(Theme.warning())
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
index 8a02e7cdc47b..5441d686df5d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
@@ -248,6 +248,8 @@ class ChaosTab extends AbstractTableTab {
                     .text(Text.from(Line.from(Span.raw("")), 
Line.from(Span.styled("  " + why, Theme.muted()))))
                     .block(block(" Chaos "))
                     .build(), area);
+            // such as why L does nothing here
+            control.render(frame, area);
             return;
         }
         if (explore) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
index 8247f935b2e6..accfdea91b6f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/chaos.md
@@ -78,9 +78,11 @@ Esc shows the rules again.
 
 **L** starts an exploration here, by hand: pick the rules to try from the
 suggestions of the integration (the same as **w**), the most worth trying
-first, each with what should hold despite its faults. The single faults
-not tried yet are picked, of routes that get messages by themselves (a
-timer, or a route they are called from); a route whose messages come from
+first, each with what should hold despite its faults. Picked are a few
+faults not tried yet, each kind once before it is tried again in another
+route, and then two faults at once (an outage while the dead letter
+channel fails); only of routes that get messages by themselves (a timer,
+or a route they are called from). A route whose messages come from
 outside is marked *needs messages*: send it some while its rule runs, or
 its verdict says nothing. Space picks a rule, Left and Right change the
 budget (2m to 30m), and Enter starts. The picked rules are tried one at a
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java
index dc2c1e5414f5..b0813916ce48 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreTest.java
@@ -34,6 +34,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.awaitility.Awaitility.await;
 
 /**
@@ -210,6 +211,34 @@ class ChaosExploreTest {
         assertThat(footer()).contains("L", "let there be chaos");
     }
 
+    @Test
+    void aKindOfFaultOnceBeforeItIsTriedAgainInAnotherRoute() throws Exception 
{
+        JsonObject reply = (JsonObject) Jsoner.deserialize("""
+                {"suggestions": [
+                  {"route": "small", "kind": "lost", "title": "The route after 
a hand-off fails",
+                   "rule": "route=a error expect=small->a:lost<=0", "feed": 
{"kind": "self", "fed": true}},
+                  {"route": "medium", "kind": "lost", "title": "The route 
after a hand-off fails",
+                   "rule": "route=b error expect=medium->b:lost<=0", "feed": 
{"kind": "self", "fed": true}},
+                  {"route": "large", "kind": "lost", "title": "The route after 
a hand-off fails",
+                   "rule": "route=c error expect=large->c:lost<=0", "feed": 
{"kind": "self", "fed": true}},
+                  {"route": "small", "kind": "errors", "title": "Returns an 
error (HTTP status)",
+                   "rule": "endpoint=http://x error status=503 
expect=small:failed<=0", "feed": {"kind": "self", "fed": true}}
+                ]}
+                """);
+        ChaosExploreDialog dialog = new ChaosExploreDialog("shop", reply);
+        // the HTTP error before a second hand-off, then the hand-offs again 
while there is room
+        assertThat(dialog.picked()).extracting(ChaosExploreDialog.Item::route, 
ChaosExploreDialog.Item::kind)
+                .containsExactly(tuple("small", "lost"), tuple("medium", 
"lost"), tuple("small", "errors"));
+    }
+
+    @Test
+    void lSaysWhyWhenChaosIsOff() {
+        info.chaos = null;
+        key('L');
+        assertThat(TuiTestHelper.renderToString(tab, 160, 40))
+                .contains("camel-chaos is not in shop (yet): run it with camel 
run --chaos");
+    }
+
     @Test
     void oneExplorationAtATime() throws Exception {
         info.chaos = ChaosInfo.parse((JsonObject) Jsoner.deserialize("""

Reply via email to