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 3fe7a44af422 CAMEL-26725: camel-jbang - make a test of a chaos rule
with AI, so a finding runs again with the same faults (#29395)
3fe7a44af422 is described below
commit 3fe7a44af42202ebb56ee2c3b9fc6e6815ebb087
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Nov 8 01:04:23 2026 +0100
CAMEL-26725: camel-jbang - make a test of a chaos rule with AI, so a
finding runs again with the same faults (#29395)
t on a rule of the chaos screen asks the AI panel to make a test of it: a
JUnit test with ChaosAssertions for a Maven project, or a scenario with an
expectation and the camel run --chaos command for a camel run folder.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/chaos.adoc | 5 +-
components/camel-chaos/src/main/docs/chaos.adoc | 5 +-
.../ROOT/pages/camel-jbang-tui-observe.adoc | 5 ++
.../dsl/jbang/core/commands/tui/ChaosAiPrompt.java | 55 +++++++++++++++++++++-
.../dsl/jbang/core/commands/tui/ChaosControl.java | 3 ++
.../dsl/jbang/core/commands/tui/ChaosTab.java | 6 +++
.../src/main/resources/tui/help/chaos.md | 8 ++++
.../jbang/core/commands/tui/ChaosAiPromptTest.java | 47 +++++++++++++++++-
8 files changed, 130 insertions(+), 4 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 7479bbdd7d16..b98acd6eed9d 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
@@ -257,7 +257,10 @@ of a scenario says which one it came from (`(scenario
outage.chaos)` in the text
The chaos screen of the Camel TUI saves the rules there are as a scenario (key
`s`), with the seed: rules tried by hand
become a file of the project, to run again. On its Errors tab, `!` makes a
rule of a real error, which fails the same
way again (the same exception and message at the same step, or the same HTTP
status at the same endpoint, ten times):
-fix the bug, then prove the fix against the same failure, and keep the rule in
a scenario.
+fix the bug, then prove the fix against the same failure, and keep the rule in
a scenario. And `t` on a rule asks its
+AI panel to make a test of it: a JUnit test with `ChaosAssertions` for a Maven
project, or a scenario with an
+expectation and the `camel run --chaos=<file> --max-seconds=<seconds>`
command, whose exit code is the verdict, for a
+folder that runs with `camel run`.
== Rules
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 7479bbdd7d16..b98acd6eed9d 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -257,7 +257,10 @@ of a scenario says which one it came from (`(scenario
outage.chaos)` in the text
The chaos screen of the Camel TUI saves the rules there are as a scenario (key
`s`), with the seed: rules tried by hand
become a file of the project, to run again. On its Errors tab, `!` makes a
rule of a real error, which fails the same
way again (the same exception and message at the same step, or the same HTTP
status at the same endpoint, ten times):
-fix the bug, then prove the fix against the same failure, and keep the rule in
a scenario.
+fix the bug, then prove the fix against the same failure, and keep the rule in
a scenario. And `t` on a rule asks its
+AI panel to make a test of it: a JUnit test with `ChaosAssertions` for a Maven
project, or a scenario with an
+expectation and the `camel run --chaos=<file> --max-seconds=<seconds>`
command, whose exit code is the verdict, for a
+folder that runs with `camel run`.
== Rules
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 3ce16ed356b6..18ac3e5c79c4 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
@@ -338,6 +338,11 @@ fails the same way again (see
<<_troubleshooting_errors,Troubleshooting Errors>>
image::jbang/camel-tui-chaos-here.png[! on the to callStock of the Diagram:
the known rules for its HTTP endpoint and its step, with the endpoint filled in]
* *x* removes the selected rule, *c* removes all rules (the report keeps what
they did), and *R* starts a new experiment:
no rules and a new report, with the same seed.
+* *t* makes a test of the selected rule with AI, so the finding runs again
with the same faults. The AI panel opens with
+the rule, the seed and what the rule did. For a Maven project the AI writes a
JUnit test with chaos switched on and
+`ChaosAssertions` (see the camel-chaos documentation); for a folder that runs
with `camel run`, a scenario with an
+expectation and the `camel run --chaos=<file> --max-seconds=<seconds>` command
whose exit code is the verdict. It adds
+an expectation when the rule has none, and asks before it writes the file.
* *s* saves the rules as a scenario in the folder of the integration:
`chaos/<name>.chaos` unless you type another file,
with the seed and one rule per line, so the experiment can run again with
`camel cmd chaos-load` or
`camel run --chaos=<file>`. A file that is there already is replaced only when
you say yes.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
index b9a0fb741d0a..70ef721e3740 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPrompt.java
@@ -16,6 +16,8 @@
*/
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;
@@ -24,7 +26,7 @@ import java.util.List;
* error a rule made on the Errors tab), CAMEL-26702: the rule, its live state
and the counts of the routes, and what to
* do with them - say in plain words what the fault simulates and what the
routes did with it, then suggest how they
* should cope, and change the routes only when the user agrees. It is put in
the input of the panel, for the user to
- * send with Enter or change first.
+ * send with Enter or change first. The question to make a test of a rule
({@code t}, CAMEL-26725) is put there too.
*/
final class ChaosAiPrompt {
@@ -48,6 +50,57 @@ final class ChaosAiPrompt {
return sb.toString();
}
+ /**
+ * The question to make a test of a rule ({@code t} on the Chaos screen,
CAMEL-26725), so the finding runs again
+ * with the same faults: the rule, the seed and what the rule did, then a
JUnit test with {@code ChaosAssertions}
+ * for a Maven project, or a scenario with an expectation and the run
whose exit code is the verdict for a folder
+ * that runs with {@code camel run}. The AI writes the file with the file
tools of the panel, which ask first.
+ */
+ static String ofTest(IntegrationInfo info, ChaosInfo.Rule rule) {
+ long seed = info.chaos != null ? info.chaos.seed : 0;
+ StringBuilder sb = new StringBuilder();
+ sb.append("Make a test of chaos rule ").append(rule.id).append(" of
the running integration ").append(name(info))
+ .append(", so this finding runs again with the same faults.
The rule (camel-chaos, injected on purpose):\n")
+ .append(" ").append(rule.text).append('\n').append("The seed
of its random draws: ").append(seed)
+ .append(".\n");
+ state(sb, info, rule);
+ String expectation = rule.expectRoute != null
+ ? "The rule has an expectation: keep it."
+ : "The rule has no expectation (expect=): add one that says
what must hold, such as"
+ + " expect=<route>:<count> within=10s (at least that many
exchanges complete in the route) or"
+ + " expect=<route>:handled<=0 (no message goes to the dead
letter channel).";
+ if (isMavenProject(info)) {
+ sb.append("The integration is a Maven project. Read its routes
first, then write a JUnit test in src/test/java")
+ .append(" that runs them with chaos switched on: add
org.apache.camel:camel-chaos as a test dependency")
+ .append(" when the pom.xml does not have it, and set the
properties camel.chaos.enabled=true,")
+ .append(" camel.chaos.seed=").append(seed).append(" and
camel.chaos.rules=<the rule> (CamelTestSupport:")
+ .append(" override configureContext and call
config.withUseOverridePropertiesWithPropertiesComponent(props);")
+ .append(" a camel-test-main-junit6 test sets the same
properties in its configuration). ")
+ .append(expectation)
+ .append(" Send the messages the test needs to the route
(replace a consumer that needs a broker or a")
+ .append(" server with a direct endpoint, using
AdviceWith), and assert with")
+ .append("
org.apache.camel.component.chaos.ChaosAssertions: assertFired(context,
\"").append(rule.id)
+ .append("\", <at least>), assertExpectationMet(context,
\"").append(rule.id)
+ .append("\", Duration.ofSeconds(10)) and
assertVerdict(context, ChaosReport.PASS, Duration.ofSeconds(10)).");
+ } else {
+ sb.append("The integration runs with camel run, without a Maven
project, so the test is a scenario with an")
+ .append(" expectation, and a run whose exit code is the
verdict. Write the scenario chaos/")
+ .append(rule.id).append("-test.chaos: a comment line that
says what it tests, seed=").append(seed)
+ .append(", and the rule. ").append(expectation)
+ .append(" Then give the command that runs it, with
--max-seconds longer than the within= of the")
+ .append(" expectation: camel run *
--chaos=chaos/").append(rule.id)
+ .append("-test.chaos --max-seconds=<seconds>
--chaos-report=target/chaos-report.json. It ends with exit")
+ .append(" code 1 when the verdict is FAIL, so it can run
in a pipeline.");
+ }
+ sb.append(" Write the file with camel_write_file, which asks me first,
and change no route.");
+ return sb.toString();
+ }
+
+ /** Whether the folder of the integration is a Maven project, which can
have a JUnit test. */
+ static boolean isMavenProject(IntegrationInfo info) {
+ return info.directory != null &&
Files.isRegularFile(Path.of(info.directory, "pom.xml"));
+ }
+
/** The most routes and sends quoted in a request, so a big integration
does not flood it. */
static final int MAX_LISTED = 20;
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 c9d794598c3b..0b4e76b4bcf4 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
@@ -633,6 +633,9 @@ final class ChaosControl {
TuiHelper.hint(spans, "R", "new experiment");
if (ctx.askAiCallback != null) {
TuiHelper.hint(spans, "d", "describe with AI");
+ if (hasRules) {
+ TuiHelper.hint(spans, "t", "make a test with AI");
+ }
}
}
}
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 de3f9b8e3d3c..06a58059ba0f 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
@@ -99,6 +99,12 @@ class ChaosTab extends AbstractTableTab {
}
return true;
}
+ if (ke.isChar('t') && rule != null && rule.id != null &&
ctx.askAiCallback != null
+ && control.step() == ChaosControl.Step.NONE) {
+ // a test of the rule, so the finding runs again with the same
faults (CAMEL-26725)
+ ctx.askAiCallback.ask(ChaosAiPrompt.ofTest(info, rule));
+ return true;
+ }
return control.handleKey(ke, info, rule);
}
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 85675d6d60fe..7e7be29d76be 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,6 +82,14 @@ error handler (HAND) and the redeliveries (REDEL).
one rule per line. Load it again with `camel cmd chaos-load`, or start
with it: `camel run --chaos=chaos/<name>.chaos`. A file that is there
already is replaced only when you say yes
+- **t** — Make a test of the selected rule with AI, so the finding runs
+ again with the same faults: the AI panel opens with a question with
+ the rule, the seed and what it did. For a Maven project the AI writes a
+ JUnit test with chaos switched on and `ChaosAssertions`; for a folder
+ that runs with `camel run` it writes a scenario with an expectation
+ (`chaos/r1-test.chaos`) and gives the `camel run --chaos=...
+ --max-seconds=...` command whose exit code is the verdict. It adds an
+ expectation when the rule has none, and asks before it writes a file
- **Shift+F8** — Explain with AI: the AI panel opens with a question about
the selected rule and what it did (its rate, its effect, the
expectation, the routes its faults were in, the recent faults). The AI
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
index c2b6f36dbe9d..c552725aa343 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosAiPromptTest.java
@@ -16,6 +16,7 @@
*/
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;
@@ -29,12 +30,13 @@ import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* CAMEL-26702: explain with AI (Shift+F8) what a chaos rule did, on the Chaos
screen and on an error a rule made on the
- * Errors tab, where it takes the place of fix with AI: the error is not a bug.
+ * Errors tab, where it takes the place of fix with AI: the error is not a
bug. CAMEL-26725: make a test of a rule (t).
*/
class ChaosAiPromptTest {
@@ -248,4 +250,47 @@ class ChaosAiPromptTest {
spans.forEach(s -> sb.append(s.content()));
return sb.toString();
}
+
+ @Test
+ void aTestOfARuleForAMavenProjectIsAJUnitTestWithTheAssertions(@TempDir
Path dir) throws Exception {
+ Files.writeString(dir.resolve("pom.xml"), "<project/>");
+ info.directory = dir.toString();
+ ChaosInfo.Rule rule = info.chaos.rules.get(0);
+
+ String question = ChaosAiPrompt.ofTest(info, rule);
+ assertThat(question).startsWith("Make a test of chaos rule r1 of the
running integration chaosdemo")
+ .contains("endpoint=http://localhost:8765/stock* error
status=503 rate=0.2 expect=orders:handled<=0")
+ .contains("The seed of its random draws: 42.")
+ .contains("Expectation: orders:handled<=0, unmet")
+ .contains("a JUnit test in
src/test/java").contains("camel.chaos.seed=42")
+ .contains("assertExpectationMet(context, \"r1\",
Duration.ofSeconds(10))")
+ .contains("The rule has an expectation: keep it.")
+ .endsWith("Write the file with camel_write_file, which asks me
first, and change no route.");
+ }
+
+ @Test
+ void aTestOfARuleForACamelRunFolderIsAScenarioAndTheRun(@TempDir Path dir)
throws Exception {
+ info.directory = dir.toString();
+ ChaosInfo.Rule rule = info.chaos.rules.get(0);
+ rule.expectRoute = null;
+
+ String question = ChaosAiPrompt.ofTest(info, rule);
+ assertThat(question).contains("runs with camel run, without a Maven
project")
+ .contains("Write the scenario
chaos/r1-test.chaos").contains("seed=42")
+ .contains("The rule has no expectation (expect=): add one")
+ .contains("camel run * --chaos=chaos/r1-test.chaos
--max-seconds=<seconds>")
+ .doesNotContain("JUnit");
+ }
+
+ @Test
+ void tOnTheChaosScreenAsksTheAiToMakeATest() {
+ ChaosTab tab = new ChaosTab(ctx);
+ TuiTestHelper.renderToString(tab, 160, 40);
+ List<Span> footer = new ArrayList<>();
+ tab.renderFooter(footer);
+ assertThat(footer.stream().map(Span::content).reduce("",
String::concat)).contains("make a test with AI");
+
+ tab.handleKeyEvent(KeyEvent.ofChar('t', KeyModifiers.NONE));
+ assertThat(asked).singleElement().asString().startsWith("Make a test
of chaos rule r1");
+ }
}