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 555c4ba5e713564405307368b297a0f5cd69c133
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 21:13:31 2026 +0100

    chore: camel-jbang - let there be chaos: a file route takes the files it 
took again (replay)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jbang/camel-tui-chaos-let-there-be-done.png    | Bin 0 -> 337934 bytes
 .../images/jbang/camel-tui-chaos-let-there-be.png  | Bin 0 -> 393165 bytes
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |  10 ++++-
 .../core/commands/tui/ChaosExploreDialog.java      |  47 +++++++++++++++++----
 .../jbang/core/commands/tui/ChaosExploreRun.java   |   8 +++-
 .../src/main/resources/tui/help/chaos.md           |   5 ++-
 .../jbang/core/commands/tui/ChaosExploreTest.java  |  37 ++++++++++++++++
 7 files changed, 93 insertions(+), 14 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be-done.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be-done.png
new file mode 100644
index 000000000000..b311fb5d460b
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be-done.png
 differ
diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be.png
new file mode 100644
index 000000000000..39375677ff57
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-let-there-be.png 
differ
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 8ca57af15b05..69a9cb5184e3 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
@@ -397,13 +397,19 @@ Press *L* to start an exploration by hand, on the 
integration you look at: pick
 integration suggests, the most worth trying first, each with what should hold 
despite its faults, and a budget of 2 to
 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
+themselves (a timer, or a route they are called from), or that take files and 
took some already: while the rule runs,
+the files the route moved to `.camel` are put back into its folder as it takes 
them, the same files each time (a route with
+`noop=true` never takes a file of the same name again, so it is not one of 
them). 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
 `camel cmd chaos-explore`.
 
+image::jbang/camel-tui-chaos-let-there-be.png[Let there be chaos: the rules to 
pick, among them an outage of the route a circuit breaker calls, the HTTP calls 
of a toD, and the routes that take the files a file route took again]
+
+image::jbang/camel-tui-chaos-let-there-be-done.png[A rule tried for two 
minutes: the same three files taken again and again, and 45 orders lost at the 
seda hand-off between two routes]
+
 image::jbang/camel-tui-chaos-explore.png[The exploration of the chaos example 
on the Chaos tab: six experiments in five minutes on a strip of time, and the 
failed one in detail: 2 orders lost at the dead letter channel]
 
 === CVE Audit
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 65c21a00d316..6d634cc4044b 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
@@ -62,12 +62,29 @@ final class ChaosExploreDialog {
     /**
      * A suggestion to try.
      *
-     * @param rule   its rules, separated by ; for two faults at once
-     * @param expect what should hold, such as orders:failed<=0
-     * @param fed    whether its route gets messages without a feed (a timer, 
or a route it is called from)
+     * @param rule        its rules, separated by ; for two faults at once
+     * @param expect      what should hold, such as orders:failed<=0
+     * @param fed         whether its route gets messages without a feed (a 
timer, or a route it is called from)
+     * @param replay      how many files the file route took, which are put 
back so it takes them again (0 for none)
+     * @param replayRoute the file route whose files are put back: the route, 
or a file route that sends to it
      */
     record Item(String route, String kind, String title, String rule, String 
why, String expect, boolean tried,
-            boolean fed) {
+            boolean fed, int replay, String replayRoute) {
+
+        Item(String route, String kind, String title, String rule, String why, 
String expect, boolean tried,
+             boolean fed) {
+            this(route, kind, title, rule, why, expect, tried, fed, 0, null);
+        }
+
+        /** Whether its route gets messages while the rule runs: by itself, or 
from the files it took, put back. */
+        boolean hasMessages() {
+            return fed || replay > 0;
+        }
+
+        /** Whether the files the route took are put back when the rule 
starts. */
+        boolean replays() {
+            return !fed && replay > 0;
+        }
 
         boolean combined() {
             return "combined".equals(kind);
@@ -108,7 +125,9 @@ final class ChaosExploreDialog {
                     items.add(new Item(
                             jo.getString("route"), jo.getString("kind"), 
jo.getString("title"), rule, jo.getString("why"),
                             m.find() ? m.group(1) : null, 
jo.getBooleanOrDefault("tried", false),
-                            feed != null && feed.getBooleanOrDefault("fed", 
false)));
+                            feed != null && feed.getBooleanOrDefault("fed", 
false),
+                            feed != null && feed.getString("replay") != null ? 
feed.getIntegerOrDefault("files", 0) : 0,
+                            feed != null ? 
feed.getStringOrDefault("replayRoute", jo.getString("route")) : null));
                 }
             }
         }
@@ -138,7 +157,7 @@ final class ChaosExploreDialog {
         }
         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()
+            boolean worth = !picked.get(i) && !item.tried() && 
item.hasMessages() && item.expect() != null && !item.deadLetter()
                     && (!distinctKinds || !kinds.contains(item.kind()));
             if (worth && (item.combined() ? combined < PICKED_COMBINED : 
singles < PICKED_SINGLE)) {
                 picked.set(i, true);
@@ -184,7 +203,7 @@ final class ChaosExploreDialog {
     /** Why the exploration cannot start, or null when it can. */
     String cannotStart() {
         if (picked().isEmpty()) {
-            return !items.isEmpty() && items.stream().noneMatch(Item::fed)
+            return !items.isEmpty() && 
items.stream().noneMatch(Item::hasMessages)
                     ? "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)";
@@ -282,7 +301,10 @@ final class ChaosExploreDialog {
             if (item.tried()) {
                 spans.add(Span.styled("  tried", Theme.success()));
             }
-            if (!item.fed()) {
+            if (item.replays()) {
+                spans.add(Span.styled("  replays " + item.replay() + 
(item.replay() == 1 ? " file" : " files"),
+                        Theme.muted()));
+            } else if (!item.fed()) {
                 spans.add(Span.styled("  needs messages", Theme.warning()));
             }
             lines.add(Line.from(spans));
@@ -311,7 +333,14 @@ final class ChaosExploreDialog {
             lines.add(Line.from(Span.styled(" A rule of this kind was tried in 
route " + item.route() + " already",
                     Theme.success())));
         }
-        if (!item.fed()) {
+        if (item.replays()) {
+            lines.add(Line.empty());
+            String who = item.route().equals(item.replayRoute()) ? "it" : 
"route " + item.replayRoute();
+            lines.add(Line.from(Span.styled(" While the rule runs, the " + 
item.replay()
+                                            + (item.replay() == 1 ? " file " : 
" files ") + who + " took are put back into"
+                                            + " its folder as it takes them, 
the same files each time",
+                    Theme.muted())));
+        } else if (!item.fed()) {
             lines.add(Line.empty());
             lines.add(Line.from(Span.styled(" Route " + item.route() + " gets 
its messages from outside: send it some"
                                             + " while the rule runs, or the 
verdict says nothing (INCONCLUSIVE)",
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java
index 9a8e39801c47..6dd1ebb8ba0e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosExploreRun.java
@@ -141,8 +141,11 @@ final class ChaosExploreRun {
                 ChaosExploreDialog.Item pick = picks.get(i);
                 String verdict;
                 try {
+                    // a file route that does not get messages by itself takes 
the files it took again
                     JsonObject ended = ChaosExperimentRun.run(integration, 
pick.route() + ": " + pick.title(),
-                            forShare(pick.rule(), share), null, share + GRACE, 
null);
+                            forShare(pick.rule(), share),
+                            pick.replays() ? 
ChaosExperimentRun.Feed.replay(pick.replayRoute()) : null,
+                            share + GRACE, null);
                     verdict = ended.getString("verdict");
                     if (ended.getLongOrDefault("timeLeft", 1) <= 0) {
                         // the deadline of the exploration: camel-chaos has 
ended it
@@ -275,7 +278,8 @@ final class ChaosExploreRun {
 
             @Override
             public JsonObject send(ChaosExperimentRun.Feed feed) {
-                // the exploration of the TUI sends no messages: it tries the 
rules of routes that get them by themselves
+                // the exploration of the TUI sends no messages: its routes 
get them by themselves, or take the files they
+                // took again (a replay, which is a chaos command)
                 throw new UnsupportedOperationException("No feed");
             }
 
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 accfdea91b6f..98b4085650a9 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
@@ -82,7 +82,10 @@ 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
+or a route they are called from), or that take files and took some
+already: those are marked *replays 3 files*, and while the rule runs the
+files the route moved to `.camel` are put back into its folder as it
+takes them, the same files each time. 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 b0813916ce48..d5b613b184b1 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
@@ -23,6 +23,9 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Consumer;
 
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.Span;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
@@ -194,6 +197,38 @@ class ChaosExploreTest {
         assertThat(TuiTestHelper.renderToString(tab, 160, 40)).contains("of 
10m: 3 rules tried, 3 FAIL");
     }
 
+    @Test
+    void aFileRouteTakesTheFilesItTookAgain() throws Exception {
+        ChaosExploreDialog dialog = new ChaosExploreDialog(
+                "shop", (JsonObject) Jsoner.deserialize(
+                        """
+                                {"suggestions": [
+                                  {"route": "inbox", "kind": "errors", 
"title": "Fails to write",
+                                   "rule": "endpoint=file://archive error 
exception=java.io.IOException rate=0.25 until=10m expect=inbox:failed<=0",
+                                   "why": "a write", "feed": {"kind": "send", 
"replay": "/work/inbox/.camel", "files": 3, "replayRoute": "files"}},
+                                  {"route": "uploads", "kind": "consumer", 
"title": "The route fails",
+                                   "rule": "route=uploads error rate=0.05 
until=10m expect=uploads:failed<=0",
+                                   "why": "an input from outside", "feed": 
{"kind": "file"}}
+                                ]}
+                                """));
+        // the files it took are put back, so it has messages; a route that 
took none needs them from outside
+        
assertThat(dialog.picked()).extracting(ChaosExploreDialog.Item::route).containsExactly("inbox");
+        assertThat(dialog.items().get(0).replay()).isEqualTo(3);
+        Rect area = new Rect(0, 0, 160, 30);
+        Buffer buffer = Buffer.empty(area);
+        dialog.render(Frame.forTesting(buffer), area);
+        assertThat(TuiTestHelper.bufferToString(buffer))
+                .contains("[x] Fails to write  inbox  replays 3 files", "[ ] 
The route fails  uploads  needs messages");
+
+        explorer.start("1234", dialog.picked(), "2m", 120_000);
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
!explorer.isRunning("1234"));
+        // after the rule is added, so its faults meet the files
+        assertThat(integration.commands).containsExactly(
+                "explore-start", "experiment-begin", "replay", 
"experiment-end", "explore-finish");
+        // the file route that sends to it
+        assertThat(integration.replayed).containsExactly("files");
+    }
+
     @Test
     void stopEndsTheExploration() {
         integration.decided = false;
@@ -255,6 +290,7 @@ class ChaosExploreTest {
         final List<String> commands = new CopyOnWriteArrayList<>();
         final List<String> rules = new CopyOnWriteArrayList<>();
         final List<String> hypotheses = new CopyOnWriteArrayList<>();
+        final List<String> replayed = new CopyOnWriteArrayList<>();
         volatile String budget;
         volatile boolean decided = true;
         private volatile int number;
@@ -273,6 +309,7 @@ class ChaosExploreTest {
                     hypotheses.add(request.getString("hypothesis"));
                     answer.put("number", number);
                 }
+                case "replay" -> replayed.add(request.getString("route"));
                 case "experiment-end" -> {
                     answer.put("number", number);
                     answer.put("verdict", "FAIL");

Reply via email to