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

davsclaus pushed a commit to branch CAMEL-26814-run-chaos-scenario
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 2f4f296f71476af7049e111d32fa7917b14bd6e3
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 10:55:32 2026 +0100

    CAMEL-26814: camel-jbang - the run options form picks a chaos scenario of 
the example or folder
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../images/jbang/camel-tui-run-chaos-scenario.png  | Bin 0 -> 324084 bytes
 .../pages/camel-jbang-tui-getting-started.adoc     |   5 ++
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  |   6 ++
 .../jbang/core/commands/tui/RunOptionsForm.java    |  81 ++++++++++++++++++++-
 .../src/main/resources/tui/help/chaos.md           |   3 +-
 .../core/commands/tui/RunOptionsFormTest.java      |  55 ++++++++++++++
 6 files changed, 147 insertions(+), 3 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-run-chaos-scenario.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-run-chaos-scenario.png
new file mode 100644
index 000000000000..525348c67b9b
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-run-chaos-scenario.png 
differ
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc
index 91e204bb826c..0e5fde39a487 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-getting-started.adoc
@@ -48,6 +48,11 @@ in-process in the Camel CLI JVM, which starts faster but has 
the CLI on the clas
 turn on chaos testing (`--chaos`, to inject faults with camel-chaos; not with 
profile prod), and add extra dependencies. When the port the app will listen on 
(the one you set, else 8080) is taken already, the form
 says by whom, so a second app with an HTTP server does not fail to start.
 
+When the example or folder has chaos scenarios (`test/chaos/*.chaos`), ←→ on 
the chaos testing row picks one, which
+is run with `--chaos=test/chaos/<name>.chaos`, so its rules are added at 
startup.
+
+image::jbang/camel-tui-run-chaos-scenario.png[The run options form of the 
chaos example, with chaos testing on and its payment-outage scenario picked]
+
 The example starts running in the background. The TUI auto-selects it as soon 
as it appears.
 From there you can explore tabs, watch messages flow, inspect the route 
diagram, and experiment.
 
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 70770520f41f..b1b9417faa66 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -1294,6 +1294,7 @@ class ActionsPopup {
         } else {
             runOptionsForm.open(displayName, displayName, false, true);
         }
+        
runOptionsForm.setChaosScenarios(RunOptionsForm.chaosScenarios(Path.of(folder)));
     }
 
     private void launchFromFolder() {
@@ -1322,6 +1323,9 @@ class ActionsPopup {
         } else {
             runOptionsForm.open(displayName, displayName, false, true);
         }
+        if (phantom.sourceDir != null) {
+            
runOptionsForm.setChaosScenarios(RunOptionsForm.chaosScenarios(Path.of(phantom.sourceDir)));
+        }
     }
 
     private void launchFromPhantom() {
@@ -1356,6 +1360,8 @@ class ActionsPopup {
         String baseName = 
TuiHelper.stripCategory(example.getStringOrDefault("name", ""));
         String autoName = generateUniqueName(baseName);
         runOptionsForm.open(autoName, baseName, 
ExampleHelper.isBundled(example));
+        // the example runs in a folder of its own, with its files: 
--chaos=test/chaos/<name>.chaos
+        
runOptionsForm.setChaosScenarios(RunOptionsForm.chaosScenarios(ExampleHelper.getFiles(example)));
     }
 
     private String generateUniqueName(String baseName) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index 1424843b0f5f..a5d9c520a8d9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -16,9 +16,14 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Style;
@@ -63,6 +68,8 @@ class RunOptionsForm {
     private static final int ROW_LAST = ROW_CHAOS;
     /** Height of the options page: a row per option plus the border; the 
properties page is aligned to it. */
     private static final int PAGE1_HEIGHT = ROW_COUNT + 2;
+    // the folder of the chaos scenarios of an example or project
+    private static final String CHAOS_SCENARIOS = "test/chaos";
 
     private boolean visible;
     private int page;
@@ -121,6 +128,9 @@ class RunOptionsForm {
     private int otelExportTarget; // 0=TUI, 1=Jaeger
     private boolean jfrEnabled;
     private boolean chaos;
+    // the chaos scenarios of the example or folder (test/chaos/*.chaos), and 
the one picked: 0 is none
+    private List<String> chaosScenarios = List.of();
+    private int chaosScenario;
     private boolean webConsole;
 
     private String exampleTitle;
@@ -165,6 +175,8 @@ class RunOptionsForm {
         otelExportTarget = 0;
         jfrEnabled = false;
         chaos = false;
+        chaosScenarios = List.of();
+        chaosScenario = 0;
         webConsole = false;
         selectedRow = ROW_NAME;
         page = PAGE_OPTIONS;
@@ -216,6 +228,58 @@ class RunOptionsForm {
         return portWarning;
     }
 
+    /**
+     * The chaos scenarios to offer next to Chaos testing, as --chaos takes 
them: a path relative to the folder the
+     * integration runs in, or absolute. Call after open.
+     */
+    void setChaosScenarios(List<String> scenarios) {
+        chaosScenarios = scenarios != null ? List.copyOf(scenarios) : 
List.of();
+        chaosScenario = 0;
+    }
+
+    /** The chaos scenario picked, or null for none. */
+    String chaosScenario() {
+        return chaosScenario > 0 && chaosScenario <= chaosScenarios.size() ? 
chaosScenarios.get(chaosScenario - 1) : null;
+    }
+
+    /** The chaos scenarios of an example, from the files of its catalog 
entry, as paths relative to the example. */
+    static List<String> chaosScenarios(Collection<String> files) {
+        return 
files.stream().filter(RunOptionsForm::isChaosScenario).sorted().toList();
+    }
+
+    /** The chaos scenarios of a folder (its test/chaos/*.chaos files), as 
absolute paths. */
+    static List<String> chaosScenarios(Path folder) {
+        Path dir = folder.resolve(CHAOS_SCENARIOS);
+        if (!Files.isDirectory(dir)) {
+            return List.of();
+        }
+        try (Stream<Path> files = Files.list(dir)) {
+            return files.filter(f -> Files.isRegularFile(f) && 
f.getFileName().toString().endsWith(".chaos"))
+                    .map(f -> 
f.toAbsolutePath().normalize().toString()).sorted().toList();
+        } catch (IOException e) {
+            return List.of();
+        }
+    }
+
+    private static boolean isChaosScenario(String file) {
+        return file.startsWith(CHAOS_SCENARIOS + "/") && 
file.endsWith(".chaos")
+                && file.indexOf('/', CHAOS_SCENARIOS.length() + 1) < 0;
+    }
+
+    /** The name of the scenario picked, as the file without its folder and 
.chaos, or none. */
+    private String chaosScenarioName() {
+        String scenario = chaosScenario();
+        if (scenario == null) {
+            return "none";
+        }
+        String name = Path.of(scenario).getFileName().toString();
+        return name.substring(0, name.length() - ".chaos".length());
+    }
+
+    private boolean picksChaosScenario() {
+        return chaos && !isProdProfile() && !chaosScenarios.isEmpty();
+    }
+
     /** Chaos is asked for, and the profile is not prod (where camel-chaos 
stays off). */
     boolean isChaosEnabled() {
         return chaos && !isProdProfile();
@@ -268,7 +332,9 @@ class RunOptionsForm {
             } else if (selectedRow >= ROW_CONSOLE) {
                 hint(spans, "Space", "toggle");
             }
-            if (hasProperties()) {
+            if (selectedRow == ROW_CHAOS && picksChaosScenario()) {
+                hint(spans, TuiIcons.KEY_LEFT + TuiIcons.KEY_RIGHT, 
"scenario");
+            } else if (hasProperties()) {
                 hint(spans, TuiIcons.KEY_RIGHT, "properties");
             }
             hint(spans, "Enter", "launch");
@@ -335,7 +401,8 @@ class RunOptionsForm {
         }
         // camel-chaos stays off with profile prod, so the form does not ask 
for it then
         if (isChaosEnabled()) {
-            args.add("--chaos");
+            String scenario = chaosScenario();
+            args.add(scenario != null ? "--chaos=" + scenario : "--chaos");
         }
         if (webConsole) {
             args.add("--console");
@@ -385,6 +452,12 @@ class RunOptionsForm {
             selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
             return true;
         }
+        // a chaos scenario next to Chaos testing: none, or one of them
+        if ((ke.isRight() || ke.isLeft()) && selectedRow == ROW_CHAOS && 
picksChaosScenario()) {
+            int count = chaosScenarios.size() + 1;
+            chaosScenario = (chaosScenario + (ke.isRight() ? 1 : count - 1)) % 
count;
+            return true;
+        }
         if (ke.isRight() && selectedRow == ROW_OTEL_AGENT && otelAgent) {
             otelExportTarget = (otelExportTarget + 1) % 2;
             return true;
@@ -677,6 +750,10 @@ class RunOptionsForm {
                     Span.styled(" [ ] Chaos testing (not available with 
profile prod)",
                             selectedRow == ROW_CHAOS ? 
Style.EMPTY.dim().reversed() : Style.EMPTY.dim()))),
                     new Rect(innerX, rowY, innerW, 1));
+        } else if (picksChaosScenario()) {
+            renderCheckbox(frame, innerX, rowY, innerW,
+                    "Chaos testing, scenario: ◂ " + chaosScenarioName() + " ▸",
+                    chaos, selectedRow == ROW_CHAOS);
         } else {
             renderCheckbox(frame, innerX, rowY, innerW, "Chaos testing (inject 
faults with camel-chaos)", chaos,
                     selectedRow == ROW_CHAOS);
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 98b4085650a9..d5e525ba8320 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
@@ -17,7 +17,8 @@ camel cmd chaos-load test/chaos/outage.chaos myapp
 
 A scenario (`test/chaos/outage.chaos`) keeps the rules of an experiment in
 the project: one rule per line, `#` comments and a `seed=` line.
-`camel run --chaos=test/chaos/outage.chaos` starts with one, and
+`camel run --chaos=test/chaos/outage.chaos` starts with one (in the
+run options form of F2: ←→ on Chaos testing picks one), and
 `chaos-load` loads one into a running integration (a new report).
 
 While a rule injects faults, the title bar shows a badge on every
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
index 09859c2f32cc..0d0ebc4de7de 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsFormTest.java
@@ -16,16 +16,20 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
 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;
 import dev.tamboui.tui.event.KeyModifiers;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -164,6 +168,57 @@ class RunOptionsFormTest {
         
assertThat(form.buildArgs()).contains("--profile=dev").doesNotContain("--chaos");
     }
 
+    @Test
+    void aChaosScenarioOfTheExampleIsPickedNextToChaosTesting() {
+        RunOptionsForm form = openForm();
+        
form.setChaosScenarios(RunOptionsForm.chaosScenarios(List.of("README.md", 
"chaos.camel.yaml",
+                "test/chaos/payment-outage.chaos", 
"test/chaos.citrus.it.yaml", "test/chaos/reports/run.chaos")));
+        form.handleKeyEvent(key(KeyCode.UP));
+        // offered once chaos is on
+        assertThat(renderPopupRows(form)).anyMatch(row -> row.contains("Chaos 
testing (inject faults with camel-chaos)"));
+        form.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertThat(renderPopupRows(form)).anyMatch(row -> row.contains("[x] 
Chaos testing, scenario: ◂ none ▸"));
+        List<Span> footer = new ArrayList<>();
+        form.renderFooter(footer);
+        assertThat(footer.stream().map(Span::content).reduce("", 
String::concat)).contains("scenario");
+
+        form.handleKeyEvent(key(KeyCode.RIGHT));
+        assertThat(renderPopupRows(form)).anyMatch(row -> 
row.contains("scenario: ◂ payment-outage ▸"));
+        
assertThat(form.buildArgs()).contains("--chaos=test/chaos/payment-outage.chaos").doesNotContain("--chaos");
+        // and back to none
+        form.handleKeyEvent(key(KeyCode.LEFT));
+        assertThat(form.buildArgs()).contains("--chaos");
+    }
+
+    @Test
+    void theChaosScenariosOfAFolder(@TempDir Path dir) throws Exception {
+        Files.createDirectories(dir.resolve("test/chaos/reports"));
+        Files.writeString(dir.resolve("test/chaos/outage.chaos"), 
"endpoint=http* error\n");
+        Files.writeString(dir.resolve("test/chaos/slow.chaos"), 
"endpoint=http* delay duration=1s\n");
+        Files.writeString(dir.resolve("test/chaos/notes.md"), "notes\n");
+
+        assertThat(RunOptionsForm.chaosScenarios(dir)).containsExactly(
+                
dir.resolve("test/chaos/outage.chaos").toAbsolutePath().toString(),
+                
dir.resolve("test/chaos/slow.chaos").toAbsolutePath().toString());
+        
assertThat(RunOptionsForm.chaosScenarios(dir.resolve("test"))).isEmpty();
+    }
+
+    @Test
+    void aChaosScenarioIsNotPassedWithProfileProd() {
+        RunOptionsForm form = openForm();
+        form.setChaosScenarios(List.of("test/chaos/payment-outage.chaos"));
+        form.handleKeyEvent(key(KeyCode.UP));
+        form.handleKeyEvent(KeyEvent.ofChar(' '));
+        form.handleKeyEvent(key(KeyCode.RIGHT));
+        
assertThat(form.chaosScenario()).isEqualTo("test/chaos/payment-outage.chaos");
+        // Profile is row 2: down from the last row wraps to Name, then 
Runtime, then Profile
+        form.handleKeyEvent(key(KeyCode.DOWN));
+        form.handleKeyEvent(key(KeyCode.DOWN));
+        form.handleKeyEvent(key(KeyCode.DOWN));
+        form.handleKeyEvent(KeyEvent.ofChar(' '));
+        assertThat(form.buildArgs()).contains("--profile=prod").noneMatch(a -> 
a.startsWith("--chaos"));
+    }
+
     @Test
     void optionsPageHasNoBlankRows() {
         RunOptionsForm form = openForm();

Reply via email to