This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26787-chaos-explore in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1be6bf6ed117bbfed46ff5dc7999d1e64d741ee9 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 9 17:22:28 2026 +0100 CAMEL-26787: camel-jbang - let there be chaos: the playbook as the MCP prompt camel_chaos_explore, and camel cmd chaos-explore with the AI of camel ask Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/docs/chaos.adoc | 2 + components/camel-chaos/src/main/docs/chaos.adoc | 2 + .../modules/ROOT/pages/camel-jbang-managing.adoc | 17 +- .../modules/ROOT/pages/camel-jbang-mcp.adoc | 8 + .../camel-jbang-cmd-chaos-explore.adoc | 37 ++++ .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc | 1 + .../META-INF/camel-jbang-commands-metadata.json | 2 +- .../apache/camel/dsl/jbang/core/commands/Ask.java | 7 +- .../dsl/jbang/core/commands/CamelJBangMain.java | 1 + .../dsl/jbang/core/commands/ChaosExplore.java | 207 +++++++++++++++++++++ .../core/commands/ai/ChaosExplorePlaybook.java | 108 +++++++++++ .../dsl/jbang/core/common/ChaosExperimentRun.java | 2 +- .../dsl/jbang/core/commands/ChaosExploreTest.java | 83 +++++++++ .../core/commands/ai/ChaosExplorePlaybookTest.java | 55 ++++++ .../src/test/resources/golden/cli-help.txt | 69 +++++++ .../jbang/core/commands/mcp/PromptDefinitions.java | 22 +++ .../core/commands/mcp/PromptDefinitionsTest.java | 16 ++ 17 files changed, 635 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 66d44391258b..e483e2919166 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 @@ -423,6 +423,8 @@ $ camel get chaos --report The Camel CLI runs an experiment in one call: `camel cmd chaos-experiment` begins it, sends its traffic (`--feed` with `--body`, `--repeat` and `--interval`, as `camel cmd send`), waits until its expectations are decided (at most `--wait`), and ends it. `camel cmd chaos-explore-start` and `chaos-explore-finish` start and finish the exploration. +`camel cmd chaos-explore` hands a whole exploration to an AI: it runs the experiments by itself for the budget, and +reports what held and what broke (the MCP prompt `camel_chaos_explore` is the same playbook). == Scenarios diff --git a/components/camel-chaos/src/main/docs/chaos.adoc b/components/camel-chaos/src/main/docs/chaos.adoc index 66d44391258b..e483e2919166 100644 --- a/components/camel-chaos/src/main/docs/chaos.adoc +++ b/components/camel-chaos/src/main/docs/chaos.adoc @@ -423,6 +423,8 @@ $ camel get chaos --report The Camel CLI runs an experiment in one call: `camel cmd chaos-experiment` begins it, sends its traffic (`--feed` with `--body`, `--repeat` and `--interval`, as `camel cmd send`), waits until its expectations are decided (at most `--wait`), and ends it. `camel cmd chaos-explore-start` and `chaos-explore-finish` start and finish the exploration. +`camel cmd chaos-explore` hands a whole exploration to an AI: it runs the experiments by itself for the budget, and +reports what held and what broke (the MCP prompt `camel_chaos_explore` is the same playbook). == Scenarios diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc index d41e7b421294..4720ad63b47e 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc @@ -940,7 +940,22 @@ Experiment 1 FAIL a failed refund is read again (r1, ended after 6s) The verdict is `FAIL` when an expectation is unmet (exit code 1), `PASS` when all are met, and `INCONCLUSIVE` when the wait was up before an expectation was decided. `--feed` takes the endpoint, or the id of the route, as the `feed:` line -of `chaos-suggest` has it. `camel get chaos --report` shows the exploration with its experiments. See +of `chaos-suggest` has it. `camel get chaos --report` shows the exploration with its experiments. + +`camel cmd chaos-explore` lets the AI of `camel ask` do it all: let there be chaos. It starts the exploration with the +budget, and the AI reads the suggestions, runs one experiment per call, reads each verdict, and goes on while there is +time. It asks nothing on the way, as starting the command is the consent, and it never changes the routes. When it is +done the exploration is finished, and its report and the journal of the experiments are printed (exit code 1 when an +experiment failed): + +[source,bash] +---- +camel cmd chaos-explore --budget=10m +camel cmd chaos-explore myApp --budget=1h --focus=refunds --api-type=anthropic +---- + +Run the integration with `--stub=remote`, so the experiments do not reach real systems. A route fed over HTTP or from +files cannot be fed by an experiment yet. MCP clients have the same playbook as the prompt `camel_chaos_explore`. See xref:next@components:others:chaos.adoc#_an_exploration[An exploration] in the camel-chaos documentation. === Chaos scenarios diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc index 5b5d95819925..67415f5f6890 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc @@ -1298,4 +1298,12 @@ instructions and executes each step by calling the referenced tools. | `routeId` (optional), `goal` (optional) | Guided workflow to optimize a Camel application's performance: analyze throughput, identify bottlenecks, review resource usage, and produce prioritized optimization recommendations. + +| `camel_chaos_explore` +| `budget` (optional), `focus` (optional), `name` (optional) +| Let there be chaos: the AI explores by itself how a running application with camel-chaos copes with faults, for the + `budget` (10 minutes by default). It starts an exploration, runs one experiment per call + (`camel_runtime_chaos_experiment`) from the suggestions, reads each verdict, and finishes with a report: what held, + what broke with the evidence and a hint, and what was not tried. It asks nothing on the way and never changes the + routes; camel-chaos removes the rules at the deadline. The same playbook as `camel cmd chaos-explore`. |=== diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-explore.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-explore.adoc new file mode 100644 index 000000000000..8ddbf5b6a012 --- /dev/null +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-chaos-explore.adoc @@ -0,0 +1,37 @@ + +// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE += camel cmd chaos-explore + +Let there be chaos: an AI explores by itself how a running integration copes with faults, for a time budget, and reports what held and what broke + + +== Usage + +[source,bash] +---- +camel cmd chaos-explore [options] +---- + + + +== Options + +[cols="2,5,1,2",options="header"] +|=== +| Option | Description | Default | Type +| `--api-key` | API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars | | String +| `--api-type` | API type: 'ollama', 'openai', 'anthropic', or 'watsonx' | | ApiType +| `--budget` | How long the AI explores, between 10s and 2h | 10m | String +| `--focus` | What to explore, such as a route id (default: all routes) | | String +| `--max-iterations` | Maximum number of tool-calling rounds | 100 | int +| `--model` | Model to use. Default: the provider's default; with Ollama the installed model best at tool calling | | String +| `--show-tools` | Show the tool calls and results as they happen | | boolean +| `--timeout` | Timeout in seconds for one answer of the LLM (an experiment can take minutes) | 300 | int +| `--tools` | The tools the model gets: core (a small set, for a local model), full, or auto | auto | ToolSet +| `--url` | LLM API endpoint URL, as for camel ask. Auto-detected if not specified | | String +| `--verbose` | Print debug information of the LLM calls | | boolean +| `--yes` | Let the AI send messages to endpoints the integration does not have, and run the other calls that camel ask asks for first, without asking | | boolean +| `-h,--help` | Display the help and sub-commands | | boolean +|=== + + diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc index 1c4a85c0047e..6d3b52059917 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc @@ -23,6 +23,7 @@ camel cmd [options] | xref:jbang-commands/camel-jbang-cmd-chaos-change.adoc[chaos-change] | Change a chaos rule of a running integration (camel-chaos, for development only) | xref:jbang-commands/camel-jbang-cmd-chaos-clear.adoc[chaos-clear] | Remove all chaos rules of running integrations (camel-chaos, for development only) | xref:jbang-commands/camel-jbang-cmd-chaos-experiment.adoc[chaos-experiment] | Run one chaos experiment in the exploration of a running integration: add its rules, send its traffic, wait for the verdict, and end it +| xref:jbang-commands/camel-jbang-cmd-chaos-explore.adoc[chaos-explore] | Let there be chaos: an AI explores by itself how a running integration copes with faults, for a time budget, and reports what held and what broke | xref:jbang-commands/camel-jbang-cmd-chaos-explore-finish.adoc[chaos-explore-finish] | Finish the chaos exploration of a running integration before its deadline | xref:jbang-commands/camel-jbang-cmd-chaos-explore-start.adoc[chaos-explore-start] | Start a chaos exploration in a running integration: a time to try chaos rules in, that camel-chaos enforces | xref:jbang-commands/camel-jbang-cmd-chaos-load.adoc[chaos-load] | Load a chaos scenario into a running integration (camel-chaos, for development only) diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index ccde7b460870..b7bba9ca7fc9 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -3,7 +3,7 @@ { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType": [...] { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format \"[[apigroup\/]version:]kind:[namespace\/]name\", plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": " [...] { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...] - { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] + { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...] { "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "How [...] diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java index c3a78931d857..dbdccd7ca25b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.function.Function; import org.apache.camel.dsl.jbang.core.commands.ai.AnswerChecks; @@ -128,6 +129,9 @@ public class Ask extends CamelCommand { /** Below this size (billions of parameters) a model's answers about a running integration are often wrong. */ static final double SMALL_MODEL_BILLIONS = 8; + // the tools the model does not get, such as starting a chaos exploration that camel cmd chaos-explore started + Set<String> hiddenTools = Set.of(); + private long targetPid; private AskTools askTools; // the chat's reader, or one made on the terminal for the first call to confirm @@ -182,7 +186,8 @@ public class Ask extends CamelCommand { process != null ? process.pid() : -1, process != null ? process.name() : null, core, core ? askTools.groupGuidance() : List.of()); - List<LlmClient.ToolDef> toolDefs = askTools.buildToolDefinitions(core); + List<LlmClient.ToolDef> toolDefs = new ArrayList<>(askTools.buildToolDefinitions(core)); + toolDefs.removeIf(t -> hiddenTools.contains(t.name())); if (question == null || question.isEmpty()) { return runInteractiveChat(client, process, systemPrompt, toolDefs); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index 89693244f2aa..e768d48e8c5f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -263,6 +263,7 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("chaos-change", new CommandLine(new CamelChaosChangeAction(this))) .addSubcommand("chaos-clear", new CommandLine(new CamelChaosClearAction(this))) .addSubcommand("chaos-experiment", new CommandLine(new CamelChaosExperimentAction(this))) + .addSubcommand("chaos-explore", new CommandLine(new ChaosExplore(this))) .addSubcommand("chaos-explore-finish", new CommandLine(new CamelChaosExploreFinishAction(this))) .addSubcommand("chaos-explore-start", new CommandLine(new CamelChaosExploreStartAction(this))) .addSubcommand("chaos-load", new CommandLine(new CamelChaosLoadAction(this))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java new file mode 100644 index 000000000000..5800dded6098 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ChaosExplore.java @@ -0,0 +1,207 @@ +/* + * 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.dsl.jbang.core.commands; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.camel.dsl.jbang.core.commands.ai.ChaosExplorePlaybook; +import org.apache.camel.dsl.jbang.core.commands.process.ListChaos; +import org.apache.camel.dsl.jbang.core.common.ActionClient; +import org.apache.camel.dsl.jbang.core.common.ActionClient.ActionResult; +import org.apache.camel.dsl.jbang.core.common.ChaosExperimentRun; +import org.apache.camel.dsl.jbang.core.common.CliExitCode; +import org.apache.camel.dsl.jbang.core.common.ProcessFinder; +import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess; +import org.apache.camel.dsl.jbang.core.common.RuntimeHelper; +import org.apache.camel.util.TimeUtils; +import org.apache.camel.util.json.JsonObject; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Let there be chaos (CAMEL-26787): the AI of camel ask explores chaos on a running integration by itself, for the + * budget of the user, and reports at the end. + */ +@Command(name = "chaos-explore", + description = "Let there be chaos: an AI explores by itself how a running integration copes with faults, for a" + + " time budget, and reports what held and what broke", + sortOptions = false, showDefaultValues = true, + footer = { + "%nThis command starts a chaos exploration with the budget (camel cmd chaos-explore-start), and hands", + "it to the AI of camel ask with a playbook: read the suggestions, run one experiment per call", + "(camel cmd chaos-experiment), read the verdicts, and go on while there is time. The AI asks", + "nothing on the way: starting this command is the consent to the exploration, and camel-chaos", + "removes every rule at the deadline. The AI never changes the routes. When it is done, the", + "exploration is finished, and its report and the journal of the experiments are printed. The exit", + "code is 1 when an experiment failed.", + "%nThe integration needs camel-chaos switched on (camel run --chaos), and remote endpoints are best", + "stubbed (camel run --stub=remote), so the experiments do not reach real systems.", + "%nExamples:", + " camel cmd chaos-explore", + " camel cmd chaos-explore myApp --budget=30m --focus=refunds", + " camel cmd chaos-explore --api-type=anthropic --budget=1h" }) +public class ChaosExplore extends CamelCommand { + + @Parameters(description = "Name or pid of the running Camel integration (must match one); without it, the only one" + + " running, or the only one with chaos switched on", + arity = "0..1") + String name; + + @Option(names = { "--budget" }, defaultValue = "10m", + description = "How long the AI explores, between 10s and 2h") + String budget; + + @Option(names = { "--focus" }, + description = "What to explore, such as a route id (default: all routes)") + String focus; + + @Option(names = { "--url" }, + description = "LLM API endpoint URL, as for camel ask. Auto-detected if not specified") + String url; + + @Option(names = { "--api-type" }, + description = "API type: 'ollama', 'openai', 'anthropic', or 'watsonx'") + LlmClient.ApiType apiType; + + @Option(names = { "--api-key" }, + description = "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars") + String apiKey; + + @Option(names = { "--model" }, + description = "Model to use. Default: the provider's default; with Ollama the installed model best at" + + " tool calling") + String model; + + @Option(names = { "--timeout" }, defaultValue = "300", + description = "Timeout in seconds for one answer of the LLM (an experiment can take minutes)") + int timeout = 300; + + @Option(names = { "--max-iterations" }, defaultValue = "100", + description = "Maximum number of tool-calling rounds") + int maxIterations = 100; + + @Option(names = { "--tools" }, defaultValue = "auto", + description = "The tools the model gets: core (a small set, for a local model), full, or auto") + Ask.ToolSet tools = Ask.ToolSet.auto; + + @Option(names = { "--yes" }, + description = "Let the AI send messages to endpoints the integration does not have, and run the other calls" + + " that camel ask asks for first, without asking") + boolean yes; + + @Option(names = { "--show-tools" }, + description = "Show the tool calls and results as they happen") + boolean showTools; + + @Option(names = { "--verbose" }, + description = "Print debug information of the LLM calls") + boolean verbose; + + public ChaosExplore(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doCall() throws Exception { + CamelProcess process = ProcessFinder.requireOneForChaos(name); + // the exploration has the budget of the user, not one the AI picks + ActionResult started = ActionClient.execute(process, "chaos", root -> { + root.put("command", "explore-start"); + root.put("budget", budget); + if (focus != null && !focus.isBlank()) { + root.put("focus", focus); + } + }, 10000); + if (!started.isOk()) { + printer().printErr(started.error()); + return started.exitCode(); + } + printer().println("Let there be chaos: " + process.name() + " (pid " + process.pid() + "), " + + TimeUtils.printDuration(started.reply().getLongOrDefault("budget", 0)) + + (focus != null && !focus.isBlank() ? ", focus: " + focus : "")); + int answer; + try { + answer = ask(process).doCall(); + } finally { + // the AI may have finished it already; else its rules go now + ActionClient.execute(process, "chaos", root -> root.put("command", "explore-finish"), 10000); + } + JsonObject exploration = ChaosExperimentRun.exploration(RuntimeHelper.readStatus(process.pid())); + List<String> lines = journal(exploration); + printer().println(); + lines.forEach(printer()::println); + if (failed(exploration)) { + return CliExitCode.FAILURE; + } + return answer; + } + + /** camel ask, with the playbook as its question, for the integration. */ + Ask ask(CamelProcess process) { + Ask ask = new Ask(getMain()); + ask.question = List.of(ChaosExplorePlaybook.text(ChaosExplorePlaybook.ASK, process.name(), budget, focus, true)); + ask.nameOrPid = Long.toString(process.pid()); + ask.url = url; + ask.apiType = apiType; + ask.apiKey = apiKey; + ask.model = model; + ask.timeout = timeout; + ask.maxIterations = maxIterations; + ask.tools = tools; + ask.yes = yes; + ask.showTools = showTools; + ask.showStats = true; + ask.verbose = verbose; + // the exploration runs with the budget of the user: the AI does not start another one + ask.hiddenTools = Set.of("chaos_explore_start"); + return ask; + } + + /** The journal of the exploration: how it ended, and a line per experiment with its expectations. */ + static List<String> journal(JsonObject exploration) { + List<String> lines = new ArrayList<>(); + if (exploration == null) { + lines.add("No journal of the exploration in the status of the integration"); + return lines; + } + List<Object> experiments = exploration.getCollection("experiments"); + int count = experiments != null ? experiments.size() : 0; + long failed = experiments == null + ? 0 : experiments.stream() + .filter(o -> o instanceof JsonObject jo && "FAIL".equals(jo.getString("verdict"))).count(); + long ran = exploration.getLongOrDefault("finishedAt", 0) - exploration.getLongOrDefault("startedAt", 0); + lines.add("Chaos exploration " + ("deadline".equals(exploration.getString("finishedBy")) + ? "finished at its deadline" : "finished") + + (ran > 0 ? " after " + TimeUtils.printDuration(ran) : "") + ": " + count + + (count == 1 ? " experiment" : " experiments") + ", " + failed + " failed"); + if (experiments != null) { + for (Object o : experiments) { + lines.addAll(ListChaos.experimentLines((JsonObject) o, " ")); + } + } + return lines; + } + + private static boolean failed(JsonObject exploration) { + List<Object> experiments = exploration != null ? exploration.getCollection("experiments") : null; + return experiments != null + && experiments.stream().anyMatch(o -> o instanceof JsonObject jo && "FAIL".equals(jo.getString("verdict"))); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java new file mode 100644 index 000000000000..33a80209ac75 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybook.java @@ -0,0 +1,108 @@ +/* + * 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.dsl.jbang.core.commands.ai; + +/** + * The playbook of "let there be chaos" (CAMEL-26787): what an AI does to explore chaos on an integration by itself, for + * the time of an exploration, and the report it comes back with. One text for the MCP prompt + * {@code camel_chaos_explore} and for {@code camel cmd chaos-explore} (the AI of camel ask), each with the names of its + * own tools. + * <p/> + * The AI steers on its own and asks the user nothing: the user agreed to the exploration, camel-chaos removes its rules + * at the deadline, and the AI never changes the routes. One experiment per call keeps it to about ten calls in ten + * minutes, which a small local model can do. + */ +public final class ChaosExplorePlaybook { + + /** The names of the tools the playbook tells the AI to call. */ + public record Tools(String start, String report, String suggest, String experiment, String finish, String errors) { + } + + /** The tools of the MCP server. */ + public static final Tools MCP = new Tools( + "camel_runtime_chaos_explore_start", "camel_runtime_chaos_report", "camel_runtime_chaos_suggest", + "camel_runtime_chaos_experiment", "camel_runtime_chaos_explore_finish", "camel_get_errors"); + + /** The tools of camel ask. */ + public static final Tools ASK = new Tools( + "chaos_explore_start", "chaos_report", "chaos_suggest", "chaos_experiment", "chaos_explore_finish", + "get_errors"); + + private ChaosExplorePlaybook() { + } + + /** + * The playbook. + * + * @param tools the names of the tools + * @param integration the name of the integration, or null for the one the tools select + * @param budget how long the exploration runs, such as 10m (10m when null) + * @param focus what to explore, such as a route, or null for all routes + * @param started whether the exploration is already running (camel cmd chaos-explore starts it with the budget + * of the user), else the AI starts it + */ + public static String text(Tools tools, String integration, String budget, String focus, boolean started) { + String where = integration != null && !integration.isBlank() + ? "the integration " + integration.strip() + : "the running integration"; + String time = budget != null && !budget.isBlank() ? budget.strip() : "10m"; + String about = focus != null && !focus.isBlank() + ? "Focus on " + focus.strip() + ", and the routes it hands messages to." + : "Explore all routes, the most worth trying first."; + String start = started + ? "The exploration is running (" + time + "): do not start another one." + : "Start the exploration with " + tools.start() + " (budget " + time + ")."; + return """ + Let there be chaos: explore how %s copes with faults, by yourself, for %s. %s + camel-chaos adds the faults and removes every rule at the deadline of the exploration. Do not ask \ + the user anything, and never change the routes or files: try, measure, and report at the end. + + 1. %s Then read %s: the routes and their counts before chaos (the baseline), and what was tried. + 2. Call %s for ideas: rules per route with why, and the feed of each route (how messages get in). \ + Prefer the kinds not tried yet; single faults first, then the combined ones (two faults at once). + 3. Run one experiment per idea with %s. A rule makes exchanges fail or wait on purpose, so do not \ + expect that nothing fails where the fault is: expect what should hold despite it, such as nothing \ + lost after a hand-off to another route, a failure handled by the error handler or redelivered, or the \ + route still completing its messages in time. + - hypothesis: what should hold, in a few words, such as "a failed refund is retried, not lost" + - rule: the suggested rule with a short until= (such as until=30s), and one expectation that \ + decides the hypothesis, with within= a little longer than until=: expect=<route>:failed<=0 \ + (nothing fails), expect=<route>-><next route>:lost<=0 (nothing lost between two routes), \ + expect=<route>:20 (20 completed), expect=<route>:maxTime<=5s (none slower than 5s) + - feed: when the feed of the suggestion is kind send, its endpoint, with body, repeat (such as \ + 20) and interval (such as 100..300); leave feed out when the route feeds itself (kind self). \ + Routes fed over http or from files cannot be fed by an experiment yet: try them only when they \ + feed themselves + - wait: longer than the within= of the expectation + - note: what you make of the result, in one sentence + 4. Read the result: the verdict (FAIL, PASS or INCONCLUSIVE), the expectations, what the routes \ + did, and the traffic. On FAIL, call %s for the failed exchanges and their exceptions, and say why it \ + broke. On INCONCLUSIVE, run it again with a longer wait. On PASS, move on. + 5. Go on while timeLeft of the result is more than the next experiment needs (its until= and wait, \ + plus a minute). Stop with about a minute left. + 6. Finish with %s, and write the report: + - Summary: how many experiments, how many held and how many broke + - Findings: per FAIL the route, what broke, the evidence (the expectation and its actual value, \ + the exception), and a hint for the route in terms of its error handling (a dead letter channel, \ + redelivery, onException, a circuit breaker, idempotency), not endpoint options you are not sure \ + exist; say when it looks like a bug in Camel rather than in the route + - What held: one line per PASS + - Not tried: the kinds of fault or routes left out, and why (no feed, out of time) + """.formatted(where, time, about, start, tools.report(), tools.suggest(), tools.experiment(), + tools.errors(), tools.finish()); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java index 40b0a5a1b751..742c29a7c6dc 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ChaosExperimentRun.java @@ -235,7 +235,7 @@ public final class ChaosExperimentRun { } /** The exploration in the report of the chaos console in the status, or null. */ - static JsonObject exploration(JsonObject status) { + public static JsonObject exploration(JsonObject status) { if (status == null || !(status.get("chaos") instanceof JsonObject chaos) || !(chaos.get("report") instanceof JsonObject report)) { return null; diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java new file mode 100644 index 000000000000..fa94b0c72251 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ChaosExploreTest.java @@ -0,0 +1,83 @@ +/* + * 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.dsl.jbang.core.commands; + +import java.util.List; +import java.util.Map; + +import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * camel cmd chaos-explore (CAMEL-26787): camel ask gets the playbook and the options, and the journal is printed when + * the AI is done. + */ +class ChaosExploreTest { + + @Test + void camelAskGetsThePlaybookAndTheOptions() { + ChaosExplore command = new ChaosExplore(new CamelJBangMain()); + CommandLine.populateCommand(command, "shop", "--budget=30m", "--focus=refunds", "--model=qwen3.6", + "--api-type=ollama", "--tools=core", "--show-tools"); + + Ask ask = command.ask(new CamelProcess(4711, "shop", null)); + + assertThat(ask.question).singleElement().asString() + .contains("explore how the integration shop copes", "The exploration is running (30m)", "chaos_experiment"); + assertThat(ask.nameOrPid).isEqualTo("4711"); + assertThat(ask.model).isEqualTo("qwen3.6"); + assertThat(ask.apiType).isEqualTo(LlmClient.ApiType.ollama); + assertThat(ask.tools).isEqualTo(Ask.ToolSet.core); + assertThat(ask.showTools).isTrue(); + // an experiment can take minutes, and a run many rounds + assertThat(ask.timeout).isEqualTo(300); + assertThat(ask.maxIterations).isEqualTo(100); + assertThat(ask.yes).isFalse(); + // the exploration runs with the budget of the user: the AI does not get the tool to start another one + assertThat(ask.hiddenTools).containsExactly("chaos_explore_start"); + } + + @Test + void theJournalWhenTheAiIsDone() { + JsonObject failed = new JsonObject( + Map.of("number", 1, "verdict", "FAIL", "open", false, "endedBy", "ended", + "elapsed", 31_000L, "hypothesis", "a refund never fails", "rules", new JsonArray(List.of("r1")), + "expectations", new JsonArray( + List.of(new JsonObject( + Map.of("expect", "refunds:failed<=0", "state", "unmet", "value", "4 failed")))))); + JsonObject held = new JsonObject( + Map.of("number", 2, "verdict", "PASS", "open", false, "endedBy", "ended", + "elapsed", 12_000L, "rules", new JsonArray(List.of("r2")))); + JsonObject exploration = new JsonObject( + Map.of("running", false, "finishedBy", "deadline", + "startedAt", 1_000_000L, "finishedAt", 1_180_000L, "experiments", + new JsonArray(List.of(failed, held)))); + + assertThat(ChaosExplore.journal(exploration)).containsExactly( + "Chaos exploration finished at its deadline after 3m0s: 2 experiments, 1 failed", + " 1 FAIL a refund never fails (r1, ended after 31s)", + " expect refunds:failed<=0: unmet, 4 failed", + " 2 PASS (r2, ended after 12s)"); + assertThat(ChaosExplore.journal(null)).containsExactly( + "No journal of the exploration in the status of the integration"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybookTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybookTest.java new file mode 100644 index 000000000000..df3d3d47c071 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ChaosExplorePlaybookTest.java @@ -0,0 +1,55 @@ +/* + * 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.dsl.jbang.core.commands.ai; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The playbook of "let there be chaos" (CAMEL-26787): the same steps for camel ask and the MCP server, each with the + * names of its own tools, which exist. + */ +class ChaosExplorePlaybookTest { + + @Test + void camelAskGetsItsOwnToolsAndARunningExploration() { + String text = ChaosExplorePlaybook.text(ChaosExplorePlaybook.ASK, "shop", "30m", "refunds", true); + + assertThat(text).contains("explore how the integration shop copes with faults, by yourself, for 30m", + "The exploration is running (30m): do not start another one.", "Focus on refunds", + "chaos_report", "chaos_suggest", "chaos_experiment", "chaos_explore_finish", "get_errors") + .doesNotContain("camel_runtime_", "chaos_explore_start"); + } + + @Test + void theToolsItNamesExist() { + ChaosExplorePlaybook.Tools t = ChaosExplorePlaybook.ASK; + for (String name : new String[] { t.start(), t.report(), t.suggest(), t.experiment(), t.finish(), t.errors() }) { + assertThat(ToolRegistry.findTool(name)).as(name).isNotNull(); + } + } + + @Test + void theExpectationsItTeachesAreRulesCamelChaosTakes() { + String text = ChaosExplorePlaybook.text(ChaosExplorePlaybook.MCP, null, null, null, false); + + // the forms of expect= that ChaosRuleParser reads + assertThat(text).contains("expect=<route>:failed<=0", "expect=<route>-><next route>:lost<=0", + "expect=<route>:20", "expect=<route>:maxTime<=5s", "within="); + } +} 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 2900be01a02a..db328bcb784c 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 @@ -561,6 +561,9 @@ Commands: chaos-experiment Run one chaos experiment in the exploration of a running integration: add its rules, send its traffic, wait for the verdict, and end it + chaos-explore Let there be chaos: an AI explores by itself how a + running integration copes with faults, for a time + budget, and reports what held and what broke chaos-explore-finish Finish the chaos exploration of a running integration before its deadline chaos-explore-start Start a chaos exploration in a running integration: a @@ -823,6 +826,72 @@ failed<=0 within=70s' \ --wait=90s camel cmd chaos-experiment --end --note='stopped by hand' +== camel cmd chaos-explore --help +Usage: camel cmd chaos-explore [-h] [--show-tools] [--verbose] [--yes] + [--api-key=<apiKey>] [--api-type=<apiType>] + [--budget=<budget>] [--focus=<focus>] + [--max-iterations=<maxIterations>] + [--model=<model>] [--timeout=<timeout>] + [--tools=<tools>] [--url=<url>] [<name>] +Let there be chaos: an AI explores by itself how a running integration copes +with faults, for a time budget, and reports what held and what broke + [<name>] Name or pid of the running Camel integration (must + match one); without it, the only one running, or + the only one with chaos switched on + -h, --help Display the help and sub-commands + --budget=<budget> How long the AI explores, between 10s and 2h + Default: 10m + --focus=<focus> What to explore, such as a route id (default: all + routes) + --url=<url> LLM API endpoint URL, as for camel ask. + Auto-detected if not specified + --api-type=<apiType> API type: 'ollama', 'openai', 'anthropic', or + 'watsonx' + --api-key=<apiKey> API key. Also reads ANTHROPIC_API_KEY, + OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY + env vars + --model=<model> Model to use. Default: the provider's default; + with Ollama the installed model best at tool + calling + --timeout=<timeout> Timeout in seconds for one answer of the LLM (an + experiment can take minutes) + Default: 300 + --max-iterations=<maxIterations> + Maximum number of tool-calling rounds + Default: 100 + --tools=<tools> The tools the model gets: core (a small set, for a + local model), full, or auto + Default: auto + --yes Let the AI send messages to endpoints the + integration does not have, and run the other + calls that camel ask asks for first, without + asking + --show-tools Show the tool calls and results as they happen + --verbose Print debug information of the LLM calls + +This command starts a chaos exploration with the budget (camel cmd +chaos-explore-start), and hands +it to the AI of camel ask with a playbook: read the suggestions, run one +experiment per call +(camel cmd chaos-experiment), read the verdicts, and go on while there is time. +The AI asks +nothing on the way: starting this command is the consent to the exploration, +and camel-chaos +removes every rule at the deadline. The AI never changes the routes. When it is +done, the +exploration is finished, and its report and the journal of the experiments are +printed. The exit +code is 1 when an experiment failed. + +The integration needs camel-chaos switched on (camel run --chaos), and remote +endpoints are best +stubbed (camel run --stub=remote), so the experiments do not reach real systems. + +Examples: + camel cmd chaos-explore + camel cmd chaos-explore myApp --budget=30m --focus=refunds + camel cmd chaos-explore --api-type=anthropic --budget=1h + == camel cmd chaos-explore-finish --help Usage: camel cmd chaos-explore-finish [-h] [--timeout=<timeout>] [<name>] Finish the chaos exploration of a running integration before its deadline diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java index 2cb4a1371637..1cafc11a8f34 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java @@ -23,6 +23,7 @@ import jakarta.enterprise.context.ApplicationScoped; import io.quarkiverse.mcp.server.Prompt; import io.quarkiverse.mcp.server.PromptArg; import io.quarkiverse.mcp.server.PromptMessage; +import org.apache.camel.dsl.jbang.core.commands.ai.ChaosExplorePlaybook; /** * MCP Prompt definitions that provide structured multi-step workflows for LLMs. @@ -382,4 +383,25 @@ public class PromptDefinitions { return List.of(PromptMessage.withUserRole(instructions)); } + + /** + * Let there be chaos (CAMEL-26787): the AI explores chaos on a running integration by itself for a time budget, one + * experiment per call, and reports at the end. The playbook is the one of camel cmd chaos-explore. + */ + @Prompt(name = "camel_chaos_explore", + description = "Let there be chaos: explore by yourself how a running Camel application copes with faults, " + + "for a time budget: start a chaos exploration, run one experiment per call (rules, traffic " + + "and the wait for the verdict), and report what held and what broke, with hints. " + + "Needs camel-chaos switched on (camel run --chaos).") + public List<PromptMessage> chaosExplore( + @PromptArg(name = "budget", description = "How long to explore, such as 10m or 1h (default 10m, at most 2h)", + required = false) String budget, + @PromptArg(name = "focus", description = "What to explore, such as a route id (default: all routes)", + required = false) String focus, + @PromptArg(name = "name", description = "Name or PID of the Camel application (default: the only one" + + " running, or the only one with chaos switched on)", + required = false) String name) { + return List.of(PromptMessage.withUserRole( + ChaosExplorePlaybook.text(ChaosExplorePlaybook.MCP, name, budget, focus, false))); + } } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java index 5ae648d67beb..79a25fd392e9 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitionsTest.java @@ -265,6 +265,22 @@ class PromptDefinitionsTest { assertThat(mentioned).as("tools named by the prompts").isSubsetOf(tools); } + // ---- camel_chaos_explore ---- + + @Test + void chaosExploreIsThePlaybookWithTheMcpTools() { + String text = extractText(prompts.chaosExplore("1h", "refunds", "shop")); + + assertThat(text).contains("Let there be chaos: explore how the integration shop copes with faults, by" + + " yourself, for 1h", + "Focus on refunds", + "Start the exploration with camel_runtime_chaos_explore_start (budget 1h)", + "camel_runtime_chaos_suggest", "camel_runtime_chaos_experiment", "camel_runtime_chaos_explore_finish", + "Do not ask the user anything"); + assertThat(extractText(prompts.chaosExplore(null, null, null))) + .contains("the running integration copes with faults, by yourself, for 10m", "Explore all routes"); + } + // ---- helper ---- private String extractText(List<PromptMessage> messages) {
