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

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

commit f521fba5183fde95e59b6e9844312e710e22746b
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 16:42:16 2026 +0100

    CAMEL-26695: camel-jbang - chaos screen: add a rule from the known rules of 
the chaos model, with values picked from the integration
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/images/jbang/camel-tui-chaos-recipes.png  | Bin 0 -> 420859 bytes
 .../ROOT/pages/camel-jbang-tui-observe.adoc        |  11 +-
 .../dsl/jbang/core/commands/tui/ChaosControl.java  |  70 ++++-
 .../jbang/core/commands/tui/ChaosRecipeForm.java   | 334 +++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/ChaosRecipes.java  | 204 +++++++++++++
 .../src/main/resources/tui/help/chaos.md           |   8 +-
 .../core/commands/tui/ChaosRecipeFormTest.java     | 174 +++++++++++
 .../jbang/core/commands/tui/ChaosRecipesTest.java  | 147 +++++++++
 8 files changed, 942 insertions(+), 6 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-recipes.png 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-recipes.png
new file mode 100644
index 000000000000..61ce3a1d58ff
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-chaos-recipes.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 be83c83f8c23..c2b2a32e5b12 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
@@ -263,8 +263,15 @@ on purpose is not debugged as a real one.
 The rules are added, changed and removed on the tab, as with `camel cmd 
chaos-add`, `chaos-change`, `chaos-remove` and
 `chaos-clear` (see 
xref:next@components:others:chaos.adoc#_control_while_it_runs[Control while it 
runs]):
 
-* *a* adds a rule, typed as one line (`endpoint=kafka:orders* error rate=0.1 
until=5m`), and *e* changes the selected
-rule, starting from the rule as it is.
+* *a* adds a rule. It lists the known rules of camel-chaos by what goes wrong 
(Downstream: returns an error, is slow,
+times out, did the work and then failed; Messages; Consumers and routes; Error 
handling), from the chaos model of the
+catalog. For the one chosen, a field per value of its rule: the endpoints, 
routes and steps of the integration are
+picked with Left and Right (for "Returns an error (HTTP status)", the HTTP 
endpoints the routes call), the other
+values start with a default, and the rule is shown as it is built. The last 
entry types the rule as one line
+(`endpoint=kafka:orders* error rate=0.1 until=5m`).
++
+image::jbang/camel-tui-chaos-recipes.png[Adding a chaos rule from the known 
rules: the HTTP endpoint of the route is picked]
+* *e* changes the selected rule, starting from the rule as it is.
 * *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.
 
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 20b13bfab658..701cb2e422ed 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
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Function;
+import java.util.function.Supplier;
 
 import dev.tamboui.layout.Rect;
 import dev.tamboui.style.Overflow;
@@ -27,6 +28,7 @@ import dev.tamboui.terminal.Frame;
 import dev.tamboui.text.Line;
 import dev.tamboui.text.Span;
 import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.widgets.Clear;
 import dev.tamboui.widgets.block.Block;
@@ -50,6 +52,8 @@ final class ChaosControl {
     /** What the control shows: nothing, the rule being typed, the question, 
or the outcome to read. */
     enum Step {
         NONE,
+        // the known rules to pick from, and the form of the one picked
+        RECIPES,
         INPUT,
         CONFIRM,
         RESULT
@@ -67,6 +71,11 @@ final class ChaosControl {
     private final MonitorContext ctx;
     // sends the request of the chaos action to the integration and returns 
its reply (null when it did not answer)
     private final Function<JsonObject, JsonObject> sender;
+    // the known rules of the catalog, or null when its catalog has none 
(loaded once, when a rule is first added)
+    private final Supplier<ChaosRecipes> recipeSource;
+    private ChaosRecipes recipes;
+    private boolean recipesLoaded;
+    private ChaosRecipeForm form;
     // set by the background task that sends the action, read when the screen 
is drawn
     private volatile Step step = Step.NONE;
     private Command command;
@@ -80,12 +89,21 @@ final class ChaosControl {
     private boolean resultError;
 
     ChaosControl(MonitorContext ctx) {
-        this(ctx, null);
+        this(ctx, null, ChaosRecipes::load);
     }
 
     ChaosControl(MonitorContext ctx, Function<JsonObject, JsonObject> sender) {
+        this(ctx, sender, () -> null);
+    }
+
+    ChaosControl(MonitorContext ctx, Function<JsonObject, JsonObject> sender, 
Supplier<ChaosRecipes> recipeSource) {
         this.ctx = ctx;
         this.sender = sender;
+        this.recipeSource = recipeSource;
+    }
+
+    ChaosRecipeForm form() {
+        return form;
     }
 
     Step step() {
@@ -93,7 +111,8 @@ final class ChaosControl {
     }
 
     boolean isTextInputActive() {
-        return step == Step.INPUT;
+        // the list of known rules takes the keys too, so a digit or a letter 
does not leave the screen
+        return step == Step.INPUT || step == Step.RECIPES;
     }
 
     boolean isConfirming() {
@@ -113,6 +132,9 @@ final class ChaosControl {
      */
     boolean handleKey(KeyEvent ke, IntegrationInfo info, ChaosInfo.Rule 
selected) {
         switch (step) {
+            case RECIPES -> {
+                return handleRecipeKey(ke);
+            }
             case INPUT -> {
                 return handleInputKey(ke);
             }
@@ -148,7 +170,15 @@ final class ChaosControl {
         name = info.name;
         if (ke.isChar('a')) {
             id = null;
-            openInput(Command.ADD, "");
+            ChaosRecipes known = knownRules();
+            if (known != null) {
+                // what can go wrong, in plain words, with the endpoints, 
routes and steps of the integration
+                command = Command.ADD;
+                form = new ChaosRecipeForm(known, info);
+                step = Step.RECIPES;
+            } else {
+                openInput(Command.ADD, "");
+            }
             return true;
         }
         if (selected != null && selected.id != null && ke.isChar('e')) {
@@ -173,6 +203,29 @@ final class ChaosControl {
         return false;
     }
 
+    private ChaosRecipes knownRules() {
+        if (!recipesLoaded) {
+            recipes = recipeSource.get();
+            recipesLoaded = true;
+        }
+        return recipes;
+    }
+
+    private boolean handleRecipeKey(KeyEvent ke) {
+        switch (form.handleKey(ke)) {
+            case CLOSED -> close();
+            case TYPE_RULE -> openInput(Command.ADD, "");
+            case RULE -> {
+                rule = form.rule();
+                ask(Command.ADD);
+            }
+            default -> {
+                // the form used the key
+            }
+        }
+        return true;
+    }
+
     private void openInput(Command what, String text) {
         command = what;
         input = new TextInputState(text != null ? text : "");
@@ -197,6 +250,10 @@ final class ChaosControl {
 
     /** Pastes into the rule being typed. */
     boolean handlePaste(String text) {
+        if (step == Step.RECIPES) {
+            form.paste(text);
+            return true;
+        }
         if (step != Step.INPUT || text == null) {
             return false;
         }
@@ -219,6 +276,10 @@ final class ChaosControl {
         if (step == Step.NONE) {
             return false;
         }
+        if (step == Step.RECIPES) {
+            // the form goes back to the list, the list closes
+            return handleRecipeKey(KeyEvent.ofKey(KeyCode.ESCAPE));
+        }
         close();
         return true;
     }
@@ -226,6 +287,7 @@ final class ChaosControl {
     private void close() {
         step = Step.NONE;
         input = null;
+        form = null;
     }
 
     /** The request of the chaos action for the command, as {@code camel cmd 
chaos-*} sends it. */
@@ -333,6 +395,7 @@ final class ChaosControl {
 
     void render(Frame frame, Rect area) {
         switch (step) {
+            case RECIPES -> form.render(frame, area);
             case INPUT -> DialogHelper.renderInputDialog(frame, area,
                     command == Command.CHANGE ? "Change chaos rule " + id : 
"Add a chaos rule to " + name, input,
                     "endpoint=kafka:orders* error rate=0.1 until=5m");
@@ -408,6 +471,7 @@ final class ChaosControl {
     /** The keys of the screen, or of the open dialog, for the footer. */
     void renderFooter(List<Span> spans, boolean hasRules) {
         switch (step) {
+            case RECIPES -> form.renderFooter(spans);
             case INPUT -> {
                 TuiHelper.hint(spans, "Enter", "confirm");
                 TuiHelper.hint(spans, "Esc", "cancel");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
new file mode 100644
index 000000000000..4459cae0814e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeForm.java
@@ -0,0 +1,334 @@
+/*
+ * 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.tui;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Borders;
+import dev.tamboui.widgets.input.TextInputState;
+import dev.tamboui.widgets.paragraph.Paragraph;
+
+/**
+ * Adding a chaos rule from the known rules ({@link ChaosRecipes}): a list of 
what can go wrong, grouped, and for the
+ * chosen one a field per placeholder of its rule. A field with values to pick 
from (the endpoints, routes or steps of
+ * the integration) cycles through them with Left and Right, and can still be 
typed; the other fields start with the
+ * default of the recipe. The rule is shown as it is built, and Enter takes it 
to the confirmation of
+ * {@link ChaosControl}. The last entry of the list types the whole rule 
instead.
+ */
+final class ChaosRecipeForm {
+
+    /** What the form gives back after a key. */
+    enum Outcome {
+        // the key was used; nothing to do
+        NONE,
+        // Esc on the list: close
+        CLOSED,
+        // the last entry of the list: type the rule as one line
+        TYPE_RULE,
+        // the rule is complete: confirm it
+        RULE
+    }
+
+    /** A field of the form: the placeholder, its value, and the values to 
pick from. */
+    static final class Field {
+        final ChaosRecipes.Placeholder placeholder;
+        final TextInputState input;
+        final List<String> choices;
+        int choice = -1;
+
+        Field(ChaosRecipes.Placeholder placeholder, List<String> choices) {
+            this.placeholder = placeholder;
+            this.choices = choices;
+            String start = placeholder.defaultValue() != null ? 
placeholder.defaultValue() : "";
+            if (start.isEmpty() && !choices.isEmpty()) {
+                choice = 0;
+                start = choices.get(0);
+            }
+            this.input = new TextInputState(start);
+            input.moveCursorToEnd();
+        }
+
+        void cycle(int step) {
+            if (choices.isEmpty()) {
+                return;
+            }
+            choice = Math.floorMod(choice + step, choices.size());
+            input.clear();
+            input.insert(choices.get(choice));
+        }
+
+        String value() {
+            return input.text().strip();
+        }
+    }
+
+    private static final String TYPE_IT = "Write the rule yourself …";
+
+    private final List<ChaosRecipes.Recipe> recipes;
+    private final IntegrationInfo info;
+    // the index in the list (the recipes, then the entry that types the rule)
+    private int selected;
+    private boolean inForm;
+    private final List<Field> fields = new ArrayList<>();
+    private int focus;
+    private String rule;
+
+    ChaosRecipeForm(ChaosRecipes recipes, IntegrationInfo info) {
+        this.recipes = recipes.recipes();
+        this.info = info;
+    }
+
+    boolean inForm() {
+        return inForm;
+    }
+
+    /** The rule of the form, with the values filled in. */
+    String rule() {
+        return rule;
+    }
+
+    ChaosRecipes.Recipe selectedRecipe() {
+        return selected < recipes.size() ? recipes.get(selected) : null;
+    }
+
+    List<Field> fields() {
+        return fields;
+    }
+
+    Outcome handleKey(KeyEvent ke) {
+        return inForm ? handleFormKey(ke) : handleListKey(ke);
+    }
+
+    private Outcome handleListKey(KeyEvent ke) {
+        if (ke.isCancel()) {
+            return Outcome.CLOSED;
+        }
+        if (ke.isUp()) {
+            selected = Math.max(0, selected - 1);
+        } else if (ke.isDown()) {
+            selected = Math.min(recipes.size(), selected + 1);
+        } else if (ke.isConfirm() || ke.isRight()) {
+            if (selected == recipes.size()) {
+                return Outcome.TYPE_RULE;
+            }
+            openForm(recipes.get(selected));
+        }
+        return Outcome.NONE;
+    }
+
+    private void openForm(ChaosRecipes.Recipe recipe) {
+        fields.clear();
+        for (ChaosRecipes.Placeholder p : recipe.placeholders()) {
+            fields.add(new Field(p, ChaosRecipes.candidates(p, info)));
+        }
+        focus = 0;
+        inForm = true;
+    }
+
+    private Outcome handleFormKey(KeyEvent ke) {
+        if (ke.isCancel()) {
+            // back to the list
+            inForm = false;
+            return Outcome.NONE;
+        }
+        Field field = fields.isEmpty() ? null : fields.get(focus);
+        if (ke.isUp()) {
+            focus = Math.max(0, focus - 1);
+        } else if (ke.isDown() || ke.isKey(KeyCode.TAB)) {
+            focus = Math.min(fields.size() - 1, focus + 1);
+        } else if (field != null && !field.choices.isEmpty() && (ke.isLeft() 
|| ke.isRight())) {
+            field.cycle(ke.isRight() ? 1 : -1);
+        } else if (ke.isConfirm()) {
+            String built = build();
+            if (missing() == null) {
+                rule = built;
+                return Outcome.RULE;
+            }
+        } else if (field != null) {
+            FormHelper.handleTextInput(ke, field.input);
+            field.choice = field.choices.indexOf(field.value());
+        }
+        return Outcome.NONE;
+    }
+
+    /** Pastes into the field in focus. */
+    void paste(String text) {
+        if (inForm && !fields.isEmpty() && text != null) {
+            fields.get(focus).input.insert(text.replace('\n', ' 
').replace('\r', ' '));
+        }
+    }
+
+    /** The rule with the values of the fields so far. */
+    String build() {
+        ChaosRecipes.Recipe recipe = selectedRecipe();
+        if (recipe == null) {
+            return "";
+        }
+        Map<String, String> values = new LinkedHashMap<>();
+        fields.forEach(f -> values.put(f.placeholder.name(), f.value()));
+        return recipe.fill(values);
+    }
+
+    /** The first field without a value, or null when the rule is complete. */
+    String missing() {
+        for (Field f : fields) {
+            if (f.value().isEmpty()) {
+                return f.placeholder.name();
+            }
+        }
+        return null;
+    }
+
+    // ---- rendering ----
+
+    void render(Frame frame, Rect area) {
+        int width = Math.min(area.width() - 4, 150);
+        int height = Math.min(area.height() - 2, 30);
+        Rect popup = DialogHelper.centered(area, width, height);
+        frame.renderWidget(Clear.INSTANCE, popup);
+        Block outer = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL).borderStyle(Theme.warning())
+                .title(" Add a chaos rule to " + info.name + " ").build();
+        frame.renderWidget(outer, popup);
+        Rect inner = outer.inner(popup);
+        List<Rect> cols = Layout.horizontal()
+                .constraints(Constraint.length(Math.min(46, inner.width() / 
3)), Constraint.fill())
+                .split(inner);
+        renderList(frame, cols.get(0));
+        renderRight(frame, cols.get(1));
+    }
+
+    private void renderList(Frame frame, Rect area) {
+        List<Line> lines = new ArrayList<>();
+        lines.add(Line.from(Span.styled(" What should go wrong?", 
Style.EMPTY.bold())));
+        String group = null;
+        int selectedLine = 0;
+        for (int i = 0; i <= recipes.size(); i++) {
+            String title;
+            if (i < recipes.size()) {
+                ChaosRecipes.Recipe r = recipes.get(i);
+                if (!r.group().equals(group)) {
+                    group = r.group();
+                    lines.add(Line.from(Span.styled(" " + group, 
Theme.muted())));
+                }
+                title = r.title();
+            } else {
+                title = TYPE_IT;
+            }
+            boolean sel = i == selected;
+            if (sel) {
+                selectedLine = lines.size();
+            }
+            Style style = sel ? (inForm ? Style.EMPTY.bold() : 
Theme.selectionBg().bold()) : Style.EMPTY;
+            lines.add(Line.from(Span.styled((sel ? " > " : "   ") + title, 
style)));
+        }
+        // keep the selected recipe in sight
+        int scroll = Math.max(0, selectedLine - (area.height() - 2));
+        
frame.renderWidget(Paragraph.builder().text(Text.from(lines)).scroll(scroll)
+                
.block(Block.builder().borders(EnumSet.of(Borders.RIGHT)).borderStyle(Theme.border()).build()).build(),
 area);
+    }
+
+    private void renderRight(Frame frame, Rect area) {
+        ChaosRecipes.Recipe recipe = selectedRecipe();
+        Rect pad = new Rect(area.x() + 1, area.y(), Math.max(1, area.width() - 
2), area.height());
+        if (recipe == null) {
+            
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(
+                    Line.from(Span.styled(" Type the rule as one line, such 
as", Style.EMPTY)),
+                    Line.from(Span.styled(" endpoint=kafka:orders* error 
rate=0.1 until=5m", Theme.muted()))))
+                    .build(), pad);
+            return;
+        }
+        List<Line> top = new ArrayList<>();
+        top.add(Line.from(Span.styled(recipe.title(), 
Theme.warning().bold())));
+        top.add(Line.from(Span.styled(recipe.description() != null ? 
recipe.description() : "", Theme.muted())));
+        top.add(Line.empty());
+        int topHeight = 4 + (recipe.description() != null ? 
recipe.description().length() / Math.max(1, pad.width()) : 0);
+        
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(top)).build(),
+                new Rect(pad.x(), pad.y(), pad.width(), Math.min(topHeight, 
pad.height())));
+        int y = pad.y() + topHeight;
+        if (!inForm) {
+            
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(
+                    Line.from(Span.styled(recipe.rule(), 
Theme.muted())))).build(),
+                    new Rect(pad.x(), y, pad.width(), Math.max(1, pad.bottom() 
- y)));
+            return;
+        }
+        int labelWidth = 12;
+        for (int i = 0; i < fields.size() && y < pad.bottom() - 4; i++, y++) {
+            Field f = fields.get(i);
+            boolean focused = i == focus;
+            
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(Span.styled(
+                    String.format("%-" + labelWidth + "s", 
f.placeholder.name()),
+                    focused ? Theme.warning().bold() : 
Theme.muted())))).build(),
+                    new Rect(pad.x(), y, labelWidth, 1));
+            String hint = f.choices.isEmpty()
+                    ? ""
+                    : "  ◂ ▸ " + (f.choice >= 0 ? (f.choice + 1) + " of " + 
f.choices.size() : f.choices.size() + " to pick");
+            int fieldWidth = Math.max(10, pad.width() - labelWidth - 
hint.length() - 1);
+            FormHelper.renderTextField(frame, new Rect(pad.x() + labelWidth, 
y, fieldWidth, 1), f.input, focused,
+                    f.placeholder.example());
+            if (!hint.isEmpty()) {
+                
frame.renderWidget(Paragraph.builder().text(Text.from(Line.from(Span.styled(hint,
 Theme.muted())))).build(),
+                        new Rect(pad.x() + labelWidth + fieldWidth, y, 
hint.length(), 1));
+            }
+        }
+        Field focusedField = fields.isEmpty() ? null : fields.get(focus);
+        List<Line> bottom = new ArrayList<>();
+        if (focusedField != null && focusedField.placeholder.description() != 
null) {
+            
bottom.add(Line.from(Span.styled(focusedField.placeholder.description(), 
Theme.muted())));
+        }
+        bottom.add(Line.empty());
+        bottom.add(Line.from(Span.styled(build(), Theme.info())));
+        String missing = missing();
+        bottom.add(missing == null
+                ? Line.from(Span.styled("Enter to add it (asks to confirm)", 
Theme.success()))
+                : Line.from(Span.styled(missing + " needs a value", 
Theme.warning())));
+        int bottomY = y + 1;
+        
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(bottom)).build(),
+                new Rect(pad.x(), bottomY, pad.width(), Math.max(1, 
pad.bottom() - bottomY)));
+    }
+
+    /** The keys of the form, for the footer. */
+    void renderFooter(List<Span> spans) {
+        if (inForm) {
+            TuiHelper.hint(spans, "↑↓", "field");
+            TuiHelper.hint(spans, "←→", "pick");
+            TuiHelper.hint(spans, "Enter", "add");
+            TuiHelper.hint(spans, "Esc", "back");
+        } else {
+            TuiHelper.hint(spans, "↑↓", "choose");
+            TuiHelper.hint(spans, "Enter", "fill in");
+            TuiHelper.hint(spans, "Esc", "cancel");
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipes.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipes.java
new file mode 100644
index 000000000000..624d1cc883d1
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipes.java
@@ -0,0 +1,204 @@
+/*
+ * 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.tui;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The known rules of camel-chaos (the recipes of the chaos model of the 
catalog, {@code CamelCatalog.chaosModelJSon()},
+ * CAMEL-26689): what goes wrong in plain words, each a one-line rule with 
{@code ${name}} placeholders, and where to
+ * pick the value of a placeholder from in the integration (the endpoints its 
routes send to, the endpoints it polls,
+ * its routes, its steps, or listed values).
+ */
+final class ChaosRecipes {
+
+    /** A placeholder of a recipe. */
+    record Placeholder(String name, String type, String description, String 
defaultValue, String example, String pickFrom,
+            List<String> schemes, List<String> values) {
+    }
+
+    /** A known rule. */
+    record Recipe(String id, String group, String title, String description, 
String rule, List<Placeholder> placeholders) {
+
+        /** The rule with the values of the placeholders; a placeholder 
without a value stays {@code ${name}}. */
+        String fill(Map<String, String> values) {
+            String answer = rule;
+            for (Placeholder p : placeholders) {
+                String value = values.get(p.name());
+                if (value != null && !value.isBlank()) {
+                    answer = answer.replace("${" + p.name() + "}", 
value.strip());
+                }
+            }
+            return answer;
+        }
+    }
+
+    private final List<Recipe> recipes;
+
+    ChaosRecipes(List<Recipe> recipes) {
+        this.recipes = List.copyOf(recipes);
+    }
+
+    List<Recipe> recipes() {
+        return recipes;
+    }
+
+    /**
+     * The recipes of the catalog of the Camel CLI, or null when its catalog 
has no chaos model (a Camel version before
+     * it).
+     */
+    static ChaosRecipes load() {
+        try {
+            String json = new DefaultCamelCatalog().chaosModelJSon();
+            return json != null ? parse(json) : null;
+        } catch (RuntimeException | LinkageError e) {
+            // an older catalog: the rule is typed as one line
+            return null;
+        }
+    }
+
+    /** The recipes of the chaos model ({@code {"chaos": {"recipes": 
[...]}}}). */
+    static ChaosRecipes parse(String json) {
+        JsonObject root;
+        try {
+            root = (JsonObject) Jsoner.deserialize(json);
+        } catch (Exception e) {
+            return null;
+        }
+        if (!(root.get("chaos") instanceof JsonObject chaos) || 
!(chaos.get("recipes") instanceof JsonArray array)) {
+            return null;
+        }
+        List<Recipe> answer = new ArrayList<>();
+        for (Object o : array) {
+            if (!(o instanceof JsonObject r) || r.getString("rule") == null) {
+                continue;
+            }
+            List<Placeholder> placeholders = new ArrayList<>();
+            if (r.get("placeholders") instanceof JsonArray ps) {
+                for (Object po : ps) {
+                    if (po instanceof JsonObject p) {
+                        JsonObject pick = p.get("pick") instanceof JsonObject 
pj ? pj : null;
+                        placeholders.add(new Placeholder(
+                                p.getString("name"), p.getString("type"), 
p.getString("description"),
+                                p.getString("default"), p.getString("example"),
+                                pick != null ? pick.getString("from") : null,
+                                strings(pick != null ? pick.get("schemes") : 
null),
+                                strings(pick != null ? pick.get("values") : 
null)));
+                    }
+                }
+            }
+            answer.add(new Recipe(
+                    r.getString("id"), r.getStringOrDefault("group", ""), 
r.getString("title"),
+                    r.getString("description"), r.getString("rule"), 
placeholders));
+        }
+        return answer.isEmpty() ? null : new ChaosRecipes(answer);
+    }
+
+    private static List<String> strings(Object value) {
+        List<String> answer = new ArrayList<>();
+        if (value instanceof List<?> list) {
+            list.forEach(v -> answer.add(String.valueOf(v)));
+        }
+        return answer;
+    }
+
+    /**
+     * The values a placeholder can be picked from in the integration: the 
endpoints its routes send to (of the schemes
+     * of the placeholder, when it names some), the endpoints it consumes with 
a scheduled poll, its routes, its steps
+     * (the ones with an id of their own first), or the listed values. Empty 
when the value is typed.
+     */
+    static List<String> candidates(Placeholder p, IntegrationInfo info) {
+        Set<String> answer = new LinkedHashSet<>();
+        if (p.pickFrom() == null) {
+            return List.of();
+        }
+        switch (p.pickFrom()) {
+            case "values" -> answer.addAll(p.values());
+            case "producers" -> {
+                for (ProducerInfo producer : info.producers) {
+                    addUri(answer, producer.uri, p.schemes());
+                }
+                for (EndpointInfo endpoint : info.endpoints) {
+                    if ("out".equalsIgnoreCase(endpoint.direction)) {
+                        addUri(answer, endpoint.uri, p.schemes());
+                    }
+                }
+            }
+            case "pollingConsumers" -> {
+                for (ConsumerInfo consumer : info.consumers) {
+                    if (consumer.scheduled) {
+                        addUri(answer, consumer.uri, p.schemes());
+                    }
+                }
+            }
+            case "routes" -> info.routes.forEach(r -> {
+                if (r.routeId != null) {
+                    answer.add(r.routeId);
+                }
+            });
+            case "steps" -> {
+                List<String> named = new ArrayList<>();
+                List<String> generated = new ArrayList<>();
+                for (RouteInfo route : info.routes) {
+                    for (ProcessorInfo step : route.processors) {
+                        if (step.id != null) {
+                            (isGeneratedId(step.id) ? generated : 
named).add(step.id);
+                        }
+                    }
+                }
+                answer.addAll(named);
+                answer.addAll(generated);
+            }
+            default -> {
+                // typed (such as a header name)
+            }
+        }
+        return List.copyOf(answer);
+    }
+
+    // an id Camel made up, such as to1 or log3
+    private static boolean isGeneratedId(String id) {
+        int i = id.length();
+        while (i > 0 && Character.isDigit(id.charAt(i - 1))) {
+            i--;
+        }
+        return i < id.length() && i > 0 && id.substring(0, 
i).chars().allMatch(Character::isLowerCase);
+    }
+
+    /** Adds the URI without its query (the rule matches the endpoint with or 
without it), of one of the schemes. */
+    private static void addUri(Set<String> answer, String uri, List<String> 
schemes) {
+        if (uri == null || uri.isBlank()) {
+            return;
+        }
+        int q = uri.indexOf('?');
+        String base = q > 0 ? uri.substring(0, q) : uri;
+        int colon = base.indexOf(':');
+        String scheme = colon > 0 ? base.substring(0, colon) : base;
+        if (schemes.isEmpty() || schemes.contains(scheme)) {
+            answer.add(base);
+        }
+    }
+}
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 a8effd4f8136..456b4aa87446 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
@@ -55,7 +55,13 @@ error handler (HAND) and the redeliveries (REDEL).
 
 ## Keys
 
-- **a** — Add a rule, typed as one line, such as `endpoint=kafka:orders* error 
rate=0.1 until=5m`
+- **a** — Add a rule: pick what should go wrong from the known rules of
+  camel-chaos (such as "Downstream returns an error", "Did the work, then
+  failed", "The route flaps"), then fill in its values: the endpoints,
+  routes and steps of the integration are picked with Left and Right, and
+  the rule is shown as it is built. The last entry, "Write the rule
+  yourself", types it as one line, such as
+  `endpoint=kafka:orders* error rate=0.1 until=5m`
 - **e** — Change the selected rule: the line starts with the rule as it is
 - **x** — Remove the selected rule
 - **c** — Clear: remove all rules (the report keeps what they did)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeFormTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeFormTest.java
new file mode 100644
index 000000000000..3df8981db10e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipeFormTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+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.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 static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Adding a chaos rule from the known rules ({@link ChaosRecipeForm}), through 
the keys of the chaos screen.
+ */
+class ChaosRecipeFormTest {
+
+    private final List<JsonObject> requests = new ArrayList<>();
+    private ChaosTab tab;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        Theme.resetForTesting();
+        IntegrationInfo info = ChaosRecipesTest.chaosdemo();
+        info.chaos = ChaosInfo.parse((JsonObject) Jsoner.deserialize("""
+                {"enabled": true, "rules": [], "report": {"verdict": "PASS", 
"final": true, "seed": 42}}
+                """));
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        ctx.selectedPid = "1234";
+        ctx.confirmActions = true;
+        ChaosRecipes recipes = ChaosRecipes.parse(ChaosRecipesTest.MODEL);
+        tab = new ChaosTab(ctx, new ChaosControl(ctx, request -> {
+            requests.add(request);
+            JsonObject reply = new JsonObject();
+            reply.put("id", "r1");
+            return reply;
+        }, () -> recipes));
+    }
+
+    private void key(char ch) {
+        tab.handleKeyEvent(KeyEvent.ofChar(ch, KeyModifiers.NONE));
+    }
+
+    private void key(KeyCode code) {
+        tab.handleKeyEvent(KeyEvent.ofKey(code, KeyModifiers.NONE));
+    }
+
+    private String screen() {
+        return TuiTestHelper.renderToString(tab, 160, 40);
+    }
+
+    @Test
+    void theKnownRulesByWhatGoesWrong() {
+        key('a');
+        assertThat(tab.control().step()).isEqualTo(ChaosControl.Step.RECIPES);
+        assertThat(tab.isTextInputActive()).isTrue();
+        assertThat(screen()).contains("Add a chaos rule to 
chaosdemo").contains("What should go wrong?")
+                .contains("Downstream").contains("Returns an error (HTTP 
status)").contains("Consumers and routes")
+                .contains("The route stalls").contains("Write the rule 
yourself")
+                // the rule of the recipe in focus
+                .contains("endpoint=${endpoint} error status=${status} 
rate=${rate}");
+    }
+
+    @Test
+    void aRecipeFilledWithTheEndpointOfTheIntegrationAndItsDefaults() {
+        key('a');
+        key(KeyCode.ENTER);
+        assertThat(tab.control().form().inForm()).isTrue();
+        // the HTTP endpoint the route calls is picked, the status and the 
rate are the defaults
+        assertThat(screen()).contains("endpoint=http://localhost:8765/stock 
error status=503 rate=0.25")
+                .contains("Enter to add it");
+
+        key(KeyCode.ENTER);
+        assertThat(tab.isConfirming()).isTrue();
+        assertThat(screen()).contains("endpoint=http://localhost:8765/stock 
error status=503 rate=0.25");
+        key('y');
+        assertThat(requests).hasSize(1);
+        assertThat(requests.get(0).getString("command")).isEqualTo("add");
+        assertThat(requests.get(0).getString("rule"))
+                .isEqualTo("endpoint=http://localhost:8765/stock error 
status=503 rate=0.25");
+    }
+
+    @Test
+    void leftAndRightPickAnotherValue() {
+        key('a');
+        key(KeyCode.DOWN);
+        key(KeyCode.ENTER);
+        // route: orders, then inventory
+        assertThat(screen()).contains("route=orders stall 
duration=20s").contains("1 of 2");
+        key(KeyCode.RIGHT);
+        assertThat(screen()).contains("route=inventory stall 
duration=20s").contains("2 of 2");
+        // a value is typed too: the duration
+        key(KeyCode.DOWN);
+        for (int i = 0; i < 3; i++) {
+            key(KeyCode.BACKSPACE);
+        }
+        key('5');
+        key('s');
+        key(KeyCode.ENTER);
+        key('y');
+        
assertThat(requests.get(0).getString("rule")).isEqualTo("route=inventory stall 
duration=5s");
+    }
+
+    @Test
+    void aValueThatIsTypedMustBeThereBeforeEnter() {
+        key('a');
+        for (int i = 0; i < 3; i++) {
+            key(KeyCode.DOWN);
+        }
+        key(KeyCode.ENTER);
+        assertThat(screen()).contains("header needs a value");
+        key(KeyCode.ENTER);
+        assertThat(tab.isConfirming()).isFalse();
+        key(KeyCode.DOWN);
+        for (char ch : "X-Trace-Id".toCharArray()) {
+            key(ch);
+        }
+        key(KeyCode.ENTER);
+        assertThat(tab.isConfirming()).isTrue();
+        assertThat(screen()).contains("corrupt mode=remove-header 
header=X-Trace-Id");
+    }
+
+    @Test
+    void theLastEntryTypesTheRuleAndEscGoesBack() {
+        key('a');
+        key(KeyCode.ENTER);
+        // Esc: from the form back to the list, then closed
+        assertThat(tab.handleEscape()).isTrue();
+        assertThat(tab.control().form().inForm()).isFalse();
+        assertThat(tab.handleEscape()).isTrue();
+        assertThat(tab.control().isOpen()).isFalse();
+
+        key('a');
+        for (int i = 0; i < 10; i++) {
+            key(KeyCode.DOWN);
+        }
+        key(KeyCode.ENTER);
+        assertThat(tab.control().step()).isEqualTo(ChaosControl.Step.INPUT);
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    void theFooterSaysWhatTheKeysDo() {
+        key('a');
+        List<Span> footer = new ArrayList<>();
+        tab.renderFooter(footer);
+        assertThat(footer.stream().map(Span::content).reduce("", 
String::concat)).contains("choose").contains("fill in");
+        key(KeyCode.ENTER);
+        footer.clear();
+        tab.renderFooter(footer);
+        assertThat(footer.stream().map(Span::content).reduce("", 
String::concat)).contains("pick").contains("add");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipesTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipesTest.java
new file mode 100644
index 000000000000..6a141232fa93
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosRecipesTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.tui;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The known rules of the chaos model ({@link ChaosRecipes}): parsed from the 
catalog, filled in, and the values to pick
+ * from in an integration.
+ */
+class ChaosRecipesTest {
+
+    // the shape of the recipes of the chaos model of the catalog
+    static final String MODEL = """
+            {"chaos": {"recipes": [
+              {"id": "http-error", "group": "Downstream", "title": "Returns an 
error (HTTP status)",
+               "description": "An HTTP call fails with a status",
+               "rule": "endpoint=${endpoint} error status=${status} 
rate=${rate}",
+               "placeholders": [
+                 {"name": "endpoint", "type": "endpoint", "description": "The 
HTTP endpoint the route calls",
+                  "example": "https://inventory/stock";, "pick": {"from": 
"producers", "schemes": ["http", "https"]}},
+                 {"name": "status", "type": "integer", "description": "The 
HTTP status", "default": "503", "example": "503"},
+                 {"name": "rate", "type": "rate", "description": "How often", 
"default": "0.25", "example": "0.25"}]},
+              {"id": "stall", "group": "Consumers and routes", "title": "The 
route stalls",
+               "description": "The route is suspended for a while",
+               "rule": "route=${route} stall duration=${duration}",
+               "placeholders": [
+                 {"name": "route", "type": "route", "description": "The 
route", "example": "orders",
+                  "pick": {"from": "routes"}},
+                 {"name": "duration", "type": "duration", "description": "How 
long", "default": "20s", "example": "20s"}]},
+              {"id": "corrupt-entry", "group": "Messages", "title": "Arrives 
corrupt",
+               "rule": "route=${route} corrupt mode=${mode}",
+               "placeholders": [
+                 {"name": "route", "type": "route", "example": "orders", 
"pick": {"from": "routes"}},
+                 {"name": "mode", "type": "enum", "default": "headers", 
"example": "headers",
+                  "pick": {"from": "values", "values": ["headers", "charset", 
"empty"]}}]},
+              {"id": "missing-header", "group": "Messages", "title": "Is sent 
without a header",
+               "rule": "endpoint=${endpoint} corrupt mode=remove-header 
header=${header}",
+               "placeholders": [
+                 {"name": "endpoint", "type": "endpoint", "example": 
"kafka:orders", "pick": {"from": "producers"}},
+                 {"name": "header", "type": "string", "example": "X-Trace-Id", 
"pick": {"from": "headers"}}]},
+              {"id": "step-error", "group": "Error handling", "title": "A step 
fails",
+               "rule": "step=${step} error",
+               "placeholders": [{"name": "step", "type": "step", "example": 
"enrich", "pick": {"from": "steps"}}]},
+              {"id": "poll-error", "group": "Consumers and routes", "title": 
"Polls fail",
+               "rule": "endpoint=${endpoint} error at=poll",
+               "placeholders": [{"name": "endpoint", "type": "endpoint", 
"example": "file:inbox",
+                                 "pick": {"from": "pollingConsumers"}}]}
+            ]}}
+            """;
+
+    static IntegrationInfo chaosdemo() {
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "chaosdemo";
+        for (String uri : 
List.of("http://localhost:8765/stock?bridgeEndpoint=true";, "seda://shipping", 
"direct://inventory")) {
+            ProducerInfo producer = new ProducerInfo();
+            producer.uri = uri;
+            info.producers.add(producer);
+        }
+        ConsumerInfo timer = new ConsumerInfo();
+        timer.uri = "timer://orders?period=500";
+        ConsumerInfo files = new ConsumerInfo();
+        files.uri = "file://inbox?delay=1000";
+        files.scheduled = true;
+        info.consumers.add(timer);
+        info.consumers.add(files);
+        for (String id : List.of("orders", "inventory")) {
+            RouteInfo route = new RouteInfo();
+            route.routeId = id;
+            info.routes.add(route);
+        }
+        for (String step : List.of("to1", "callStock", "log2", "enrich")) {
+            ProcessorInfo p = new ProcessorInfo();
+            p.id = step;
+            info.routes.get(0).processors.add(p);
+        }
+        return info;
+    }
+
+    @Test
+    void theRecipesOfTheModel() {
+        ChaosRecipes recipes = ChaosRecipes.parse(MODEL);
+        assertThat(recipes.recipes()).hasSize(6);
+        ChaosRecipes.Recipe http = recipes.recipes().get(0);
+        assertThat(http.title()).isEqualTo("Returns an error (HTTP status)");
+        
assertThat(http.placeholders()).extracting(ChaosRecipes.Placeholder::name).containsExactly("endpoint",
 "status",
+                "rate");
+        
assertThat(http.placeholders().get(0).schemes()).containsExactly("http", 
"https");
+        assertThat(http.fill(Map.of("endpoint", "http://localhost:8765/stock";, 
"status", "503", "rate", "0.25")))
+                .isEqualTo("endpoint=http://localhost:8765/stock error 
status=503 rate=0.25");
+        // a placeholder without a value stays, so the form can say what is 
missing
+        assertThat(http.fill(Map.of("status", 
"503"))).startsWith("endpoint=${endpoint} error status=503");
+
+        assertThat(ChaosRecipes.parse("{\"other\": 1}")).isNull();
+        assertThat(ChaosRecipes.parse("not json")).isNull();
+    }
+
+    @Test
+    void theValuesToPickFromInTheIntegration() {
+        ChaosRecipes recipes = ChaosRecipes.parse(MODEL);
+        IntegrationInfo info = chaosdemo();
+        List<ChaosRecipes.Recipe> r = recipes.recipes();
+        // the producers of the schemes of the placeholder, without their query
+        assertThat(ChaosRecipes.candidates(r.get(0).placeholders().get(0), 
info))
+                .containsExactly("http://localhost:8765/stock";);
+        assertThat(ChaosRecipes.candidates(r.get(3).placeholders().get(0), 
info))
+                .containsExactly("http://localhost:8765/stock";, 
"seda://shipping", "direct://inventory");
+        assertThat(ChaosRecipes.candidates(r.get(1).placeholders().get(0), 
info)).containsExactly("orders", "inventory");
+        assertThat(ChaosRecipes.candidates(r.get(2).placeholders().get(1), 
info)).containsExactly("headers", "charset",
+                "empty");
+        // the steps with an id of their own first
+        assertThat(ChaosRecipes.candidates(r.get(4).placeholders().get(0), 
info))
+                .containsExactly("callStock", "enrich", "to1", "log2");
+        // the consumers that poll on a schedule
+        assertThat(ChaosRecipes.candidates(r.get(5).placeholders().get(0), 
info)).containsExactly("file://inbox");
+        // a header name is typed
+        assertThat(ChaosRecipes.candidates(r.get(3).placeholders().get(1), 
info)).isEmpty();
+    }
+
+    @Test
+    void theCatalogOfTheCliHasTheRecipes() {
+        ChaosRecipes recipes = ChaosRecipes.load();
+        assertThat(recipes).isNotNull();
+        
assertThat(recipes.recipes()).extracting(ChaosRecipes.Recipe::id).contains("http-error",
 "failed-after-success",
+                "flap");
+    }
+}

Reply via email to