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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 85d10ff730d6 CAMEL-26699: camel-chaos - the known rules say what they 
uncover, when not to use them and what to watch (#29359)
85d10ff730d6 is described below

commit 85d10ff730d6688dbff70f1f26f3d12ae67e52be
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 22:30:17 2026 +0100

    CAMEL-26699: camel-chaos - the known rules say what they uncover, when not 
to use them and what to watch (#29359)
    
    Each recipe of the chaos model now has a fuller description and three lists 
(uncovers, avoid, watch) that guide the one who picks it. The build writes them 
into chaos-model.json and the catalog, ChaosModelTest checks every recipe has 
them, and the TUI add form shows them as bullets under the recipe and while the 
rule is filled in.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/chaos-model.json      | 229 ++++++++++++++++--
 .../org/apache/camel/catalog/docs/chaos.adoc       |  16 +-
 .../org/apache/camel/chaos/chaos-model.json        | 229 ++++++++++++++++--
 components/camel-chaos/src/main/docs/chaos.adoc    |  16 +-
 .../apache/camel/component/chaos/ChaosModel.java   | 266 +++++++++++++++++++--
 .../camel/component/chaos/ChaosModelTest.java      |  15 ++
 .../jbang/core/commands/tui/ChaosRecipeForm.java   |  28 ++-
 .../dsl/jbang/core/commands/tui/ChaosRecipes.java  |  10 +-
 .../src/main/resources/tui/help/chaos.md           |   4 +-
 .../core/commands/tui/ChaosRecipeFormTest.java     |  15 ++
 .../jbang/core/commands/tui/ChaosRecipesTest.java  |   8 +
 11 files changed, 773 insertions(+), 63 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
index 7505fb834508..75d82165406c 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
@@ -492,7 +492,20 @@
         "id": "http-error",
         "group": "Downstream",
         "title": "Returns an error (HTTP status)",
-        "description": "An HTTP call fails with a status, as for a service 
that is down or overloaded: tests onException, redelivery and the dead letter 
channel",
+        "description": "An HTTP call fails with a status, as a service that is 
down (503), overloaded (429) or broken (500) answers. The exchange fails with 
the exception of the HTTP component, as the real call would",
+        "uncovers": [
+          "An error handler that does not retry, or retries a status that will 
not change, such as 400 or 404",
+          "Redeliveries without a delay, which hit an overloaded service again 
at once",
+          "Messages that are lost instead of reaching the dead letter channel"
+        ],
+        "avoid": [
+          "The endpoint is not HTTP: use Refuses the connection",
+          "To test a timeout: a status comes back at once, use Times out"
+        ],
+        "watch": [
+          "The redeliveries of the step and the exchanges handled by the error 
handler",
+          "The dead letter channel, and a circuit breaker around the call if 
there is one"
+        ],
         "rule": "endpoint=${endpoint} error status=${status} rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -538,7 +551,19 @@
         "id": "connection-refused",
         "group": "Downstream",
         "title": "Refuses the connection",
-        "description": "The send fails as when the other side is not there",
+        "description": "The send fails with a ConnectException before anything 
is sent, as when the service is not running or a firewall blocks it",
+        "uncovers": [
+          "Whether a connection error is retried: that is safe, as nothing 
reached the other side",
+          "Long redelivery delays that hold the consumer while the service is 
down",
+          "A circuit breaker that does not open"
+        ],
+        "avoid": [
+          "To test an HTTP status from a service that is up: use Returns an 
error"
+        ],
+        "watch": [
+          "The redeliveries and the exchanges handled by the error handler",
+          "The state of a circuit breaker around the call"
+        ],
         "rule": "endpoint=${endpoint} error 
exception=java.net.ConnectException rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -570,7 +595,20 @@
         "id": "slow",
         "group": "Downstream",
         "title": "Is slow",
-        "description": "Each send waits a different time within a range, as a 
slow service does",
+        "description": "Each send waits a time within the range before it is 
made, as a slow service. The exchange then goes on as usual: the cost is time, 
not errors",
+        "uncovers": [
+          "A consumer that falls behind: the throughput of the route drops and 
messages wait in front of it",
+          "Thread pools that fill up with waiting exchanges",
+          "Callers of the route (request\/reply) that time out"
+        ],
+        "avoid": [
+          "To test the timeout of the client: the wait is before the call, so 
the client's timeout does not fire. Use Times out",
+          "On a route started by a timer: the timer waits for the exchange, so 
there are fewer messages, not more load"
+        ],
+        "watch": [
+          "The mean, p95 and p99 time of the routes, and their throughput",
+          "The exchanges in flight"
+        ],
         "rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -609,7 +647,19 @@
         "id": "timeout",
         "group": "Downstream",
         "title": "Times out",
-        "description": "The send waits, then fails without calling the 
producer, as a call that times out",
+        "description": "The send waits, then fails with a 
SocketTimeoutException without calling the producer, as a call that times out",
+        "uncovers": [
+          "Whether a timeout is retried, and whether that is safe: in real 
life the other side may have done the work",
+          "How long a message takes when every attempt times out: the wait 
times the attempts",
+          "A consumer that stops taking messages while its threads wait"
+        ],
+        "avoid": [
+          "A long wait with a high rate: each fault holds a thread for that 
time"
+        ],
+        "watch": [
+          "The time of the route and the exchanges in flight",
+          "The redeliveries and the dead letter channel"
+        ],
         "rule": "endpoint=${endpoint} error after=${after} 
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -648,7 +698,19 @@
         "id": "failed-after-success",
         "group": "Downstream",
         "title": "Did the work, then failed",
-        "description": "The send succeeds and then fails, as a reply that is 
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+        "description": "The send is made and succeeds, then the exchange 
fails, as when the reply is lost. The error handler sends it again, so the 
other side gets it twice",
+        "uncovers": [
+          "Duplicates: an order booked twice, a payment taken twice",
+          "Whether the other side or the route is idempotent, such as with an 
idempotent consumer or a key the other side checks"
+        ],
+        "avoid": [
+          "Against a real service where a duplicate costs, such as one that 
charges or sends mail to people",
+          "With a high count: until=1 makes one send fail, which is enough to 
see one duplicate"
+        ],
+        "watch": [
+          "What the other side received: two of the same",
+          "The redeliveries of the step"
+        ],
         "rule": "endpoint=${endpoint} error at=reply until=${until}",
         "placeholders": [
           {
@@ -673,7 +735,19 @@
         "id": "outage",
         "group": "Downstream",
         "title": "Is down for a while",
-        "description": "Every send fails during the first part of each period, 
as outage windows",
+        "description": "Every send fails during the first part of each period 
and works for the rest, as a service that goes down and comes back",
+        "uncovers": [
+          "A circuit breaker that opens during the outage and closes after it",
+          "Redelivery that gives up before the service is back, so the 
messages go to the dead letter channel",
+          "How long the route needs to catch up after an outage"
+        ],
+        "avoid": [
+          "An outage longer than all the redeliveries together sends every 
message in it to the dead letter channel: make sure that is what you want to 
test"
+        ],
+        "watch": [
+          "The faults per second: on during the outage, off between",
+          "The state of a circuit breaker, and the exchanges handled by the 
error handler"
+        ],
         "rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period} 
until=${until}",
         "placeholders": [
           {
@@ -712,7 +786,19 @@
         "id": "corrupt-entry",
         "group": "Messages",
         "title": "Arrives corrupt",
-        "description": "The messages that enter the route are changed: random 
headers, the wrong charset, cut in half or empty",
+        "description": "The messages that enter the route are changed before 
its first step: random headers, the wrong charset, cut in half or empty",
+        "uncovers": [
+          "Validation that is missing, so a bad message gets deep into the 
route before it fails",
+          "A poison message that is redelivered again and again, instead of 
going to the dead letter channel",
+          "Unmarshal and parse errors that are not handled"
+        ],
+        "avoid": [
+          "When the body is a stream or a file: the charset and the cut change 
text and bytes only, so those messages are skipped (see the reasons it skipped)"
+        ],
+        "watch": [
+          "The errors of the steps that read the body",
+          "The messages the rule skipped, and why"
+        ],
         "rule": "route=${route} corrupt mode=${mode} rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -761,7 +847,17 @@
         "id": "empty-body",
         "group": "Messages",
         "title": "Is sent with an empty body",
-        "description": "The message sent to the endpoint has an empty body",
+        "description": "The message sent to the endpoint has an empty body, as 
a sender with nothing to send",
+        "uncovers": [
+          "A service, or the next route, that does not check for an empty 
body",
+          "An empty file or message that is accepted and passed on"
+        ],
+        "avoid": [
+          "To test the route itself with bad input: use Arrives corrupt on the 
route"
+        ],
+        "watch": [
+          "The route or the service that receives it"
+        ],
         "rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -793,7 +889,17 @@
         "id": "missing-header",
         "group": "Messages",
         "title": "Is sent without a header",
-        "description": "A header is missing from the message sent to the 
endpoint",
+        "description": "A header is removed from the message sent to the 
endpoint, such as a correlation id, a trace id or a content type",
+        "uncovers": [
+          "A receiver that needs the header and fails without it, or worse, 
goes on and does the wrong thing",
+          "Tracing that breaks, as the trace id does not reach the next 
service"
+        ],
+        "avoid": [
+          "With a header the endpoint does not send on, such as most Camel 
headers: nothing changes"
+        ],
+        "watch": [
+          "The route or the service that receives it"
+        ],
         "rule": "endpoint=${endpoint} corrupt mode=remove-header 
header=${header} rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -834,7 +940,18 @@
         "id": "poll-error",
         "group": "Consumers and routes",
         "title": "Polls fail",
-        "description": "The polls of a scheduled poll consumer fail, as when 
the source is not reachable",
+        "description": "The polls of a scheduled poll consumer fail, as when 
the folder, the server or the database is not reachable. No exchange is made: 
the consumer polls again on its schedule",
+        "uncovers": [
+          "A consumer without a backoff, which fails every poll and fills the 
log",
+          "A health check that does not show a consumer that cannot poll"
+        ],
+        "avoid": [
+          "On a consumer that does not poll on a schedule, such as a timer, 
Kafka or JMS: the rule fires 0 times"
+        ],
+        "watch": [
+          "The errors and the backoff of the consumer",
+          "The log and the health checks"
+        ],
         "rule": "endpoint=${endpoint} error at=poll rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -866,7 +983,18 @@
         "id": "poll-skip",
         "group": "Consumers and routes",
         "title": "Polls are skipped",
-        "description": "The polls of a scheduled poll consumer are skipped, as 
a consumer that falls behind",
+        "description": "The polls of a scheduled poll consumer are skipped, as 
a consumer that falls behind: the messages wait at the source",
+        "uncovers": [
+          "Messages that pile up at the source",
+          "Alerts on a consumer that goes quiet",
+          "Batching or ordering that counts on every poll"
+        ],
+        "avoid": [
+          "On a consumer that does not poll on a schedule, such as a timer, 
Kafka or JMS: the rule fires 0 times"
+        ],
+        "watch": [
+          "The polls of the consumer, and the routes after it that get nothing"
+        ],
         "rule": "endpoint=${endpoint} drop at=poll rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -898,7 +1026,19 @@
         "id": "stall",
         "group": "Consumers and routes",
         "title": "The route stalls",
-        "description": "The route is suspended for a while, and its messages 
wait",
+        "description": "The route is suspended for the time, then resumed: its 
consumer takes no messages, and they wait in a queue or at the source",
+        "uncovers": [
+          "Senders that block or fail when the route does not take messages, 
such as direct callers or a full seda queue",
+          "Health checks and alerts for a route that stopped",
+          "How long the route needs to catch up"
+        ],
+        "avoid": [
+          "On a route others call directly and wait for: they fail while it 
stalls, which may or may not be the test"
+        ],
+        "watch": [
+          "The state of the route",
+          "The callers, and the queue in front of the route"
+        ],
         "rule": "route=${route} stall duration=${duration}",
         "placeholders": [
           {
@@ -923,7 +1063,19 @@
         "id": "flap",
         "group": "Consumers and routes",
         "title": "The route flaps",
-        "description": "The route stops and starts again several times",
+        "description": "The route stops and starts again, several times, as a 
consumer that keeps losing its connection",
+        "uncovers": [
+          "Messages lost or delivered twice across a restart",
+          "Connections or other resources that leak on each start",
+          "Health checks and alerts that flap with it"
+        ],
+        "avoid": [
+          "On a route whose consumer takes long to start: make the time 
between longer than the start"
+        ],
+        "watch": [
+          "The state and the uptime of the route",
+          "The effect: completed against failed or lost messages"
+        ],
         "rule": "route=${route} lifecycle action=stop duration=${duration} 
repeat=${repeat} every=${every}",
         "placeholders": [
           {
@@ -962,7 +1114,18 @@
         "id": "removed",
         "group": "Consumers and routes",
         "title": "The route is removed",
-        "description": "The route is removed, and added back after a while",
+        "description": "The route is removed from the integration and added 
back after the time, as when one route is reloaded or redeployed",
+        "uncovers": [
+          "Routes that call it and fail while it is gone",
+          "State kept in memory that is lost with it, such as an aggregator or 
an idempotent repository"
+        ],
+        "avoid": [
+          "On a route whose state in memory you want to keep"
+        ],
+        "watch": [
+          "The callers of the route",
+          "Its counters, which start again from zero"
+        ],
         "rule": "route=${route} lifecycle action=remove duration=${duration}",
         "placeholders": [
           {
@@ -987,7 +1150,17 @@
         "id": "dlq-unreachable",
         "group": "Error handling",
         "title": "The dead letter channel is unreachable",
-        "description": "The send of the dead letter channel fails, so the 
messages it handles are lost (add it before the rule that makes them fail)",
+        "description": "The send of the dead letter channel fails, so the 
messages it should keep are lost. Add it before the rule that makes the 
messages fail",
+        "uncovers": [
+          "That the dead letter channel is a single point of failure",
+          "Whether lost messages are noticed: the report counts them as lost, 
and the verdict fails"
+        ],
+        "avoid": [
+          "On its own: without another rule, or a real error, that sends 
messages to the dead letter channel, nothing happens"
+        ],
+        "watch": [
+          "The messages lost, and the verdict"
+        ],
         "rule": "endpoint=${endpoint} error at=dlq",
         "placeholders": [
           {
@@ -1005,7 +1178,19 @@
         "id": "step-error",
         "group": "Error handling",
         "title": "A step fails",
-        "description": "A step of a route fails, as when its processor throws",
+        "description": "A step of a route fails, as when its processor throws 
because of a bug or bad data",
+        "uncovers": [
+          "onException clauses that do not match the exception",
+          "Redelivery of a step that will fail again: a bug is not a temporary 
error",
+          "Work done by the steps before it that is not undone"
+        ],
+        "avoid": [
+          "To test a call to another system: put the fault on its endpoint, 
with Returns an error or Refuses the connection"
+        ],
+        "watch": [
+          "The redeliveries of the step and the exchanges handled by the error 
handler",
+          "The errors, with the step that failed"
+        ],
         "rule": "step=${step} error rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -1037,7 +1222,17 @@
         "id": "step-slow",
         "group": "Error handling",
         "title": "A step is slow",
-        "description": "A step of a route waits before its processor is 
called",
+        "description": "A step of a route waits before its processor is 
called, as a slow lookup or calculation",
+        "uncovers": [
+          "The step that the time of the route comes from",
+          "Callers of the route that wait for it, and time out"
+        ],
+        "avoid": [
+          "In a route started by a timer: the timer waits for the exchange, so 
there are fewer messages, not more load"
+        ],
+        "watch": [
+          "The times of the steps and of the route"
+        ],
         "rule": "step=${step} delay duration=${duration} rate=${rate} 
until=${until}",
         "placeholders": [
           {
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 bfb51bdfaba6..35485f8e0688 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
@@ -625,7 +625,8 @@ Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the 
targets, the hooks o
 offers (and whether it is reached only with `at=`), the parameters of each 
fault with their types and limits, the
 options, the kinds and ops of an expectation, the limits, and recipes: known 
rules in plain words, grouped by what goes
 wrong, each a rule with placeholders (`+${endpoint}+`, `+${rate}+`) and what 
to pick them from (the endpoints the routes
-send to, the routes, the steps), such as:
+send to, the routes, the steps). To guide the one who picks a recipe, each 
says what it does, what it can uncover, when
+not to use it, and what to watch while it runs, such as:
 
 [source,json]
 ----
@@ -633,6 +634,19 @@ send to, the routes, the steps), such as:
   "id": "failed-after-success",
   "group": "Downstream",
   "title": "Did the work, then failed",
+  "description": "The send is made and succeeds, then the exchange fails, as 
when the reply is lost. The error handler sends it again, so the other side 
gets it twice",
+  "uncovers": [
+    "Duplicates: an order booked twice, a payment taken twice",
+    "Whether the other side or the route is idempotent, such as with an 
idempotent consumer or a key the other side checks"
+  ],
+  "avoid": [
+    "Against a real service where a duplicate costs, such as one that charges 
or sends mail to people",
+    "With a high count: until=1 makes one send fail, which is enough to see 
one duplicate"
+  ],
+  "watch": [
+    "What the other side received: two of the same",
+    "The redeliveries of the step"
+  ],
   "rule": "endpoint=${endpoint} error at=reply until=${until}"
 }
 ----
diff --git 
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
index 7505fb834508..75d82165406c 100644
--- 
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
+++ 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
@@ -492,7 +492,20 @@
         "id": "http-error",
         "group": "Downstream",
         "title": "Returns an error (HTTP status)",
-        "description": "An HTTP call fails with a status, as for a service 
that is down or overloaded: tests onException, redelivery and the dead letter 
channel",
+        "description": "An HTTP call fails with a status, as a service that is 
down (503), overloaded (429) or broken (500) answers. The exchange fails with 
the exception of the HTTP component, as the real call would",
+        "uncovers": [
+          "An error handler that does not retry, or retries a status that will 
not change, such as 400 or 404",
+          "Redeliveries without a delay, which hit an overloaded service again 
at once",
+          "Messages that are lost instead of reaching the dead letter channel"
+        ],
+        "avoid": [
+          "The endpoint is not HTTP: use Refuses the connection",
+          "To test a timeout: a status comes back at once, use Times out"
+        ],
+        "watch": [
+          "The redeliveries of the step and the exchanges handled by the error 
handler",
+          "The dead letter channel, and a circuit breaker around the call if 
there is one"
+        ],
         "rule": "endpoint=${endpoint} error status=${status} rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -538,7 +551,19 @@
         "id": "connection-refused",
         "group": "Downstream",
         "title": "Refuses the connection",
-        "description": "The send fails as when the other side is not there",
+        "description": "The send fails with a ConnectException before anything 
is sent, as when the service is not running or a firewall blocks it",
+        "uncovers": [
+          "Whether a connection error is retried: that is safe, as nothing 
reached the other side",
+          "Long redelivery delays that hold the consumer while the service is 
down",
+          "A circuit breaker that does not open"
+        ],
+        "avoid": [
+          "To test an HTTP status from a service that is up: use Returns an 
error"
+        ],
+        "watch": [
+          "The redeliveries and the exchanges handled by the error handler",
+          "The state of a circuit breaker around the call"
+        ],
         "rule": "endpoint=${endpoint} error 
exception=java.net.ConnectException rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -570,7 +595,20 @@
         "id": "slow",
         "group": "Downstream",
         "title": "Is slow",
-        "description": "Each send waits a different time within a range, as a 
slow service does",
+        "description": "Each send waits a time within the range before it is 
made, as a slow service. The exchange then goes on as usual: the cost is time, 
not errors",
+        "uncovers": [
+          "A consumer that falls behind: the throughput of the route drops and 
messages wait in front of it",
+          "Thread pools that fill up with waiting exchanges",
+          "Callers of the route (request\/reply) that time out"
+        ],
+        "avoid": [
+          "To test the timeout of the client: the wait is before the call, so 
the client's timeout does not fire. Use Times out",
+          "On a route started by a timer: the timer waits for the exchange, so 
there are fewer messages, not more load"
+        ],
+        "watch": [
+          "The mean, p95 and p99 time of the routes, and their throughput",
+          "The exchanges in flight"
+        ],
         "rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -609,7 +647,19 @@
         "id": "timeout",
         "group": "Downstream",
         "title": "Times out",
-        "description": "The send waits, then fails without calling the 
producer, as a call that times out",
+        "description": "The send waits, then fails with a 
SocketTimeoutException without calling the producer, as a call that times out",
+        "uncovers": [
+          "Whether a timeout is retried, and whether that is safe: in real 
life the other side may have done the work",
+          "How long a message takes when every attempt times out: the wait 
times the attempts",
+          "A consumer that stops taking messages while its threads wait"
+        ],
+        "avoid": [
+          "A long wait with a high rate: each fault holds a thread for that 
time"
+        ],
+        "watch": [
+          "The time of the route and the exchanges in flight",
+          "The redeliveries and the dead letter channel"
+        ],
         "rule": "endpoint=${endpoint} error after=${after} 
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -648,7 +698,19 @@
         "id": "failed-after-success",
         "group": "Downstream",
         "title": "Did the work, then failed",
-        "description": "The send succeeds and then fails, as a reply that is 
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+        "description": "The send is made and succeeds, then the exchange 
fails, as when the reply is lost. The error handler sends it again, so the 
other side gets it twice",
+        "uncovers": [
+          "Duplicates: an order booked twice, a payment taken twice",
+          "Whether the other side or the route is idempotent, such as with an 
idempotent consumer or a key the other side checks"
+        ],
+        "avoid": [
+          "Against a real service where a duplicate costs, such as one that 
charges or sends mail to people",
+          "With a high count: until=1 makes one send fail, which is enough to 
see one duplicate"
+        ],
+        "watch": [
+          "What the other side received: two of the same",
+          "The redeliveries of the step"
+        ],
         "rule": "endpoint=${endpoint} error at=reply until=${until}",
         "placeholders": [
           {
@@ -673,7 +735,19 @@
         "id": "outage",
         "group": "Downstream",
         "title": "Is down for a while",
-        "description": "Every send fails during the first part of each period, 
as outage windows",
+        "description": "Every send fails during the first part of each period 
and works for the rest, as a service that goes down and comes back",
+        "uncovers": [
+          "A circuit breaker that opens during the outage and closes after it",
+          "Redelivery that gives up before the service is back, so the 
messages go to the dead letter channel",
+          "How long the route needs to catch up after an outage"
+        ],
+        "avoid": [
+          "An outage longer than all the redeliveries together sends every 
message in it to the dead letter channel: make sure that is what you want to 
test"
+        ],
+        "watch": [
+          "The faults per second: on during the outage, off between",
+          "The state of a circuit breaker, and the exchanges handled by the 
error handler"
+        ],
         "rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period} 
until=${until}",
         "placeholders": [
           {
@@ -712,7 +786,19 @@
         "id": "corrupt-entry",
         "group": "Messages",
         "title": "Arrives corrupt",
-        "description": "The messages that enter the route are changed: random 
headers, the wrong charset, cut in half or empty",
+        "description": "The messages that enter the route are changed before 
its first step: random headers, the wrong charset, cut in half or empty",
+        "uncovers": [
+          "Validation that is missing, so a bad message gets deep into the 
route before it fails",
+          "A poison message that is redelivered again and again, instead of 
going to the dead letter channel",
+          "Unmarshal and parse errors that are not handled"
+        ],
+        "avoid": [
+          "When the body is a stream or a file: the charset and the cut change 
text and bytes only, so those messages are skipped (see the reasons it skipped)"
+        ],
+        "watch": [
+          "The errors of the steps that read the body",
+          "The messages the rule skipped, and why"
+        ],
         "rule": "route=${route} corrupt mode=${mode} rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -761,7 +847,17 @@
         "id": "empty-body",
         "group": "Messages",
         "title": "Is sent with an empty body",
-        "description": "The message sent to the endpoint has an empty body",
+        "description": "The message sent to the endpoint has an empty body, as 
a sender with nothing to send",
+        "uncovers": [
+          "A service, or the next route, that does not check for an empty 
body",
+          "An empty file or message that is accepted and passed on"
+        ],
+        "avoid": [
+          "To test the route itself with bad input: use Arrives corrupt on the 
route"
+        ],
+        "watch": [
+          "The route or the service that receives it"
+        ],
         "rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -793,7 +889,17 @@
         "id": "missing-header",
         "group": "Messages",
         "title": "Is sent without a header",
-        "description": "A header is missing from the message sent to the 
endpoint",
+        "description": "A header is removed from the message sent to the 
endpoint, such as a correlation id, a trace id or a content type",
+        "uncovers": [
+          "A receiver that needs the header and fails without it, or worse, 
goes on and does the wrong thing",
+          "Tracing that breaks, as the trace id does not reach the next 
service"
+        ],
+        "avoid": [
+          "With a header the endpoint does not send on, such as most Camel 
headers: nothing changes"
+        ],
+        "watch": [
+          "The route or the service that receives it"
+        ],
         "rule": "endpoint=${endpoint} corrupt mode=remove-header 
header=${header} rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -834,7 +940,18 @@
         "id": "poll-error",
         "group": "Consumers and routes",
         "title": "Polls fail",
-        "description": "The polls of a scheduled poll consumer fail, as when 
the source is not reachable",
+        "description": "The polls of a scheduled poll consumer fail, as when 
the folder, the server or the database is not reachable. No exchange is made: 
the consumer polls again on its schedule",
+        "uncovers": [
+          "A consumer without a backoff, which fails every poll and fills the 
log",
+          "A health check that does not show a consumer that cannot poll"
+        ],
+        "avoid": [
+          "On a consumer that does not poll on a schedule, such as a timer, 
Kafka or JMS: the rule fires 0 times"
+        ],
+        "watch": [
+          "The errors and the backoff of the consumer",
+          "The log and the health checks"
+        ],
         "rule": "endpoint=${endpoint} error at=poll rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -866,7 +983,18 @@
         "id": "poll-skip",
         "group": "Consumers and routes",
         "title": "Polls are skipped",
-        "description": "The polls of a scheduled poll consumer are skipped, as 
a consumer that falls behind",
+        "description": "The polls of a scheduled poll consumer are skipped, as 
a consumer that falls behind: the messages wait at the source",
+        "uncovers": [
+          "Messages that pile up at the source",
+          "Alerts on a consumer that goes quiet",
+          "Batching or ordering that counts on every poll"
+        ],
+        "avoid": [
+          "On a consumer that does not poll on a schedule, such as a timer, 
Kafka or JMS: the rule fires 0 times"
+        ],
+        "watch": [
+          "The polls of the consumer, and the routes after it that get nothing"
+        ],
         "rule": "endpoint=${endpoint} drop at=poll rate=${rate} 
until=${until}",
         "placeholders": [
           {
@@ -898,7 +1026,19 @@
         "id": "stall",
         "group": "Consumers and routes",
         "title": "The route stalls",
-        "description": "The route is suspended for a while, and its messages 
wait",
+        "description": "The route is suspended for the time, then resumed: its 
consumer takes no messages, and they wait in a queue or at the source",
+        "uncovers": [
+          "Senders that block or fail when the route does not take messages, 
such as direct callers or a full seda queue",
+          "Health checks and alerts for a route that stopped",
+          "How long the route needs to catch up"
+        ],
+        "avoid": [
+          "On a route others call directly and wait for: they fail while it 
stalls, which may or may not be the test"
+        ],
+        "watch": [
+          "The state of the route",
+          "The callers, and the queue in front of the route"
+        ],
         "rule": "route=${route} stall duration=${duration}",
         "placeholders": [
           {
@@ -923,7 +1063,19 @@
         "id": "flap",
         "group": "Consumers and routes",
         "title": "The route flaps",
-        "description": "The route stops and starts again several times",
+        "description": "The route stops and starts again, several times, as a 
consumer that keeps losing its connection",
+        "uncovers": [
+          "Messages lost or delivered twice across a restart",
+          "Connections or other resources that leak on each start",
+          "Health checks and alerts that flap with it"
+        ],
+        "avoid": [
+          "On a route whose consumer takes long to start: make the time 
between longer than the start"
+        ],
+        "watch": [
+          "The state and the uptime of the route",
+          "The effect: completed against failed or lost messages"
+        ],
         "rule": "route=${route} lifecycle action=stop duration=${duration} 
repeat=${repeat} every=${every}",
         "placeholders": [
           {
@@ -962,7 +1114,18 @@
         "id": "removed",
         "group": "Consumers and routes",
         "title": "The route is removed",
-        "description": "The route is removed, and added back after a while",
+        "description": "The route is removed from the integration and added 
back after the time, as when one route is reloaded or redeployed",
+        "uncovers": [
+          "Routes that call it and fail while it is gone",
+          "State kept in memory that is lost with it, such as an aggregator or 
an idempotent repository"
+        ],
+        "avoid": [
+          "On a route whose state in memory you want to keep"
+        ],
+        "watch": [
+          "The callers of the route",
+          "Its counters, which start again from zero"
+        ],
         "rule": "route=${route} lifecycle action=remove duration=${duration}",
         "placeholders": [
           {
@@ -987,7 +1150,17 @@
         "id": "dlq-unreachable",
         "group": "Error handling",
         "title": "The dead letter channel is unreachable",
-        "description": "The send of the dead letter channel fails, so the 
messages it handles are lost (add it before the rule that makes them fail)",
+        "description": "The send of the dead letter channel fails, so the 
messages it should keep are lost. Add it before the rule that makes the 
messages fail",
+        "uncovers": [
+          "That the dead letter channel is a single point of failure",
+          "Whether lost messages are noticed: the report counts them as lost, 
and the verdict fails"
+        ],
+        "avoid": [
+          "On its own: without another rule, or a real error, that sends 
messages to the dead letter channel, nothing happens"
+        ],
+        "watch": [
+          "The messages lost, and the verdict"
+        ],
         "rule": "endpoint=${endpoint} error at=dlq",
         "placeholders": [
           {
@@ -1005,7 +1178,19 @@
         "id": "step-error",
         "group": "Error handling",
         "title": "A step fails",
-        "description": "A step of a route fails, as when its processor throws",
+        "description": "A step of a route fails, as when its processor throws 
because of a bug or bad data",
+        "uncovers": [
+          "onException clauses that do not match the exception",
+          "Redelivery of a step that will fail again: a bug is not a temporary 
error",
+          "Work done by the steps before it that is not undone"
+        ],
+        "avoid": [
+          "To test a call to another system: put the fault on its endpoint, 
with Returns an error or Refuses the connection"
+        ],
+        "watch": [
+          "The redeliveries of the step and the exchanges handled by the error 
handler",
+          "The errors, with the step that failed"
+        ],
         "rule": "step=${step} error rate=${rate} until=${until}",
         "placeholders": [
           {
@@ -1037,7 +1222,17 @@
         "id": "step-slow",
         "group": "Error handling",
         "title": "A step is slow",
-        "description": "A step of a route waits before its processor is 
called",
+        "description": "A step of a route waits before its processor is 
called, as a slow lookup or calculation",
+        "uncovers": [
+          "The step that the time of the route comes from",
+          "Callers of the route that wait for it, and time out"
+        ],
+        "avoid": [
+          "In a route started by a timer: the timer waits for the exchange, so 
there are fewer messages, not more load"
+        ],
+        "watch": [
+          "The times of the steps and of the route"
+        ],
         "rule": "step=${step} delay duration=${duration} rate=${rate} 
until=${until}",
         "placeholders": [
           {
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index bfb51bdfaba6..35485f8e0688 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -625,7 +625,8 @@ Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the 
targets, the hooks o
 offers (and whether it is reached only with `at=`), the parameters of each 
fault with their types and limits, the
 options, the kinds and ops of an expectation, the limits, and recipes: known 
rules in plain words, grouped by what goes
 wrong, each a rule with placeholders (`+${endpoint}+`, `+${rate}+`) and what 
to pick them from (the endpoints the routes
-send to, the routes, the steps), such as:
+send to, the routes, the steps). To guide the one who picks a recipe, each 
says what it does, what it can uncover, when
+not to use it, and what to watch while it runs, such as:
 
 [source,json]
 ----
@@ -633,6 +634,19 @@ send to, the routes, the steps), such as:
   "id": "failed-after-success",
   "group": "Downstream",
   "title": "Did the work, then failed",
+  "description": "The send is made and succeeds, then the exchange fails, as 
when the reply is lost. The error handler sends it again, so the other side 
gets it twice",
+  "uncovers": [
+    "Duplicates: an order booked twice, a payment taken twice",
+    "Whether the other side or the route is idempotent, such as with an 
idempotent consumer or a key the other side checks"
+  ],
+  "avoid": [
+    "Against a real service where a duplicate costs, such as one that charges 
or sends mail to people",
+    "With a high count: until=1 makes one send fail, which is enough to see 
one duplicate"
+  ],
+  "watch": [
+    "What the other side received: two of the same",
+    "The redeliveries of the step"
+  ],
   "rule": "endpoint=${endpoint} error at=reply until=${until}"
 }
 ----
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
index a8c0f2ede91e..0da00fcf3912 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
@@ -373,39 +373,44 @@ final class ChaosModel {
         // downstream
         answer.add(new Recipe(
                 "http-error", "Downstream", "Returns an error (HTTP status)",
-                "An HTTP call fails with a status, as for a service that is 
down or overloaded: tests onException,"
-                                                                              
+ " redelivery and the dead letter channel",
+                "An HTTP call fails with a status, as a service that is down 
(503), overloaded "
+                                                                              
+ "(429) or broken (500) answers. The exchange fails with the exception of the 
HTTP "
+                                                                              
+ "component, as the real call would",
                 "endpoint=${endpoint} error status=${status} rate=${rate} 
until=${until}",
                 List.of(httpEndpoint(), new Placeholder("status", "integer", 
"The HTTP status", "503", "503", null),
                         rate("0.25"), until("10m"))));
         answer.add(new Recipe(
                 "connection-refused", "Downstream", "Refuses the connection",
-                "The send fails as when the other side is not there",
+                "The send fails with a ConnectException before anything is 
sent, as when the "
+                                                                              
+ "service is not running or a firewall blocks it",
                 "endpoint=${endpoint} error 
exception=java.net.ConnectException rate=${rate} until=${until}",
                 List.of(producer(), rate("0.25"), until("10m"))));
         answer.add(new Recipe(
                 "slow", "Downstream", "Is slow",
-                "Each send waits a different time within a range, as a slow 
service does",
+                "Each send waits a time within the range before it is made, as 
a slow service. "
+                                                 + "The exchange then goes on 
as usual: the cost is time, not errors",
                 "endpoint=${endpoint} delay duration=${duration} rate=${rate} 
until=${until}",
                 List.of(producer(), new Placeholder(
                         "duration", "duration", "The time, or a range of 
times", "200ms..3s",
                         "200ms..3s", null), rate("0.5"), until("10m"))));
         answer.add(new Recipe(
                 "timeout", "Downstream", "Times out",
-                "The send waits, then fails without calling the producer, as a 
call that times out",
+                "The send waits, then fails with a SocketTimeoutException 
without calling the "
+                                                      + "producer, as a call 
that times out",
                 "endpoint=${endpoint} error after=${after} 
exception=java.net.SocketTimeoutException rate=${rate}"
-                                                                               
                      + " until=${until}",
+                                                                               
               + " until=${until}",
                 List.of(producer(), new Placeholder("after", "duration", "How 
long before it times out", "5s", "5s", null),
                         rate("0.1"), until("10m"))));
         answer.add(new Recipe(
                 "failed-after-success", "Downstream", "Did the work, then 
failed",
-                "The send succeeds and then fails, as a reply that is lost: a 
redelivery sends it again, so this tests"
-                                                                               
    + " that a duplicate does no harm",
+                "The send is made and succeeds, then the exchange fails, as 
when the reply is "
+                                                                               
    + "lost. The error handler sends it again, so the other side gets it twice",
                 "endpoint=${endpoint} error at=reply until=${until}",
                 List.of(producer(), new Placeholder("until", 
"countOrDuration", "How many sends fail", "1", "1", null))));
         answer.add(new Recipe(
                 "outage", "Downstream", "Is down for a while",
-                "Every send fails during the first part of each period, as 
outage windows",
+                "Every send fails during the first part of each period and 
works for the rest, as "
+                                                               + "a service 
that goes down and comes back",
                 "endpoint=${endpoint} error rate=burst:${outage}/${period} 
until=${until}",
                 List.of(producer(), new Placeholder("outage", "duration", "How 
long each outage lasts", "10s", "10s", null),
                         new Placeholder("period", "duration", "The time from 
one outage to the next", "1m", "1m", null),
@@ -413,8 +418,8 @@ final class ChaosModel {
         // messages
         answer.add(new Recipe(
                 "corrupt-entry", "Messages", "Arrives corrupt",
-                "The messages that enter the route are changed: random 
headers, the wrong charset, cut in half or"
-                                                                + " empty",
+                "The messages that enter the route are changed before its 
first step: random "
+                                                                + "headers, 
the wrong charset, cut in half or empty",
                 "route=${route} corrupt mode=${mode} rate=${rate} 
until=${until}",
                 List.of(route(), new Placeholder(
                         "mode", "enum", "How the message is changed", 
"headers", "headers",
@@ -422,12 +427,14 @@ final class ChaosModel {
                         rate("0.25"), until("10m"))));
         answer.add(new Recipe(
                 "empty-body", "Messages", "Is sent with an empty body",
-                "The message sent to the endpoint has an empty body",
+                "The message sent to the endpoint has an empty body, as a 
sender with nothing to "
+                                                                        + 
"send",
                 "endpoint=${endpoint} corrupt mode=empty rate=${rate} 
until=${until}",
                 List.of(producer(), rate("0.25"), until("10m"))));
         answer.add(new Recipe(
                 "missing-header", "Messages", "Is sent without a header",
-                "A header is missing from the message sent to the endpoint",
+                "A header is removed from the message sent to the endpoint, 
such as a correlation "
+                                                                          + 
"id, a trace id or a content type",
                 "endpoint=${endpoint} corrupt mode=remove-header 
header=${header} rate=${rate} until=${until}",
                 List.of(producer(), new Placeholder(
                         "header", "string", "The header, or a pattern with *", 
"X-Trace-Id",
@@ -435,60 +442,275 @@ final class ChaosModel {
         // consumers and routes
         answer.add(new Recipe(
                 "poll-error", "Consumers and routes", "Polls fail",
-                "The polls of a scheduled poll consumer fail, as when the 
source is not reachable",
+                "The polls of a scheduled poll consumer fail, as when the 
folder, the server or "
+                                                                    + "the 
database is not reachable. No exchange is made: the consumer polls again on "
+                                                                    + "its 
schedule",
                 "endpoint=${endpoint} error at=poll rate=${rate} 
until=${until}",
                 List.of(polled(), rate("0.5"), until("10m"))));
         answer.add(new Recipe(
                 "poll-skip", "Consumers and routes", "Polls are skipped",
-                "The polls of a scheduled poll consumer are skipped, as a 
consumer that falls behind",
+                "The polls of a scheduled poll consumer are skipped, as a 
consumer that falls "
+                                                                          + 
"behind: the messages wait at the source",
                 "endpoint=${endpoint} drop at=poll rate=${rate} 
until=${until}",
                 List.of(polled(), rate("0.5"), until("10m"))));
         answer.add(new Recipe(
                 "stall", "Consumers and routes", "The route stalls",
-                "The route is suspended for a while, and its messages wait",
+                "The route is suspended for the time, then resumed: its 
consumer takes no "
+                                                                     + 
"messages, and they wait in a queue or at the source",
                 "route=${route} stall duration=${duration}",
                 List.of(route(), new Placeholder("duration", "duration", "How 
long it stalls", "20s", "20s", null))));
         answer.add(new Recipe(
                 "flap", "Consumers and routes", "The route flaps",
-                "The route stops and starts again several times",
+                "The route stops and starts again, several times, as a 
consumer that keeps losing "
+                                                                   + "its 
connection",
                 "route=${route} lifecycle action=stop duration=${duration} 
repeat=${repeat} every=${every}",
                 List.of(route(), new Placeholder("duration", "duration", "How 
long it is down each time", "10s", "10s", null),
                         new Placeholder("repeat", "integer", "How many times", 
"3", "3", null),
                         new Placeholder("every", "duration", "The time from 
one time to the next", "1m", "1m", null))));
         answer.add(new Recipe(
                 "removed", "Consumers and routes", "The route is removed",
-                "The route is removed, and added back after a while",
+                "The route is removed from the integration and added back 
after the time, as when "
+                                                                           + 
"one route is reloaded or redeployed",
                 "route=${route} lifecycle action=remove duration=${duration}",
                 List.of(route(), new Placeholder("duration", "duration", "How 
long it is gone", "30s", "30s", null))));
         // error handling
         answer.add(new Recipe(
                 "dlq-unreachable", "Error handling", "The dead letter channel 
is unreachable",
-                "The send of the dead letter channel fails, so the messages it 
handles are lost (add it before the rule"
-                                                                               
                + " that makes them fail)",
+                "The send of the dead letter channel fails, so the messages it 
should keep are "
+                                                                               
                + "lost. Add it before the rule that makes the messages fail",
                 "endpoint=${endpoint} error at=dlq",
                 List.of(producer())));
         answer.add(new Recipe(
                 "step-error", "Error handling", "A step fails",
-                "A step of a route fails, as when its processor throws",
+                "A step of a route fails, as when its processor throws because 
of a bug or bad "
+                                                                + "data",
                 "step=${step} error rate=${rate} until=${until}",
                 List.of(step(), rate("0.1"), until("10m"))));
         answer.add(new Recipe(
                 "step-slow", "Error handling", "A step is slow",
-                "A step of a route waits before its processor is called",
+                "A step of a route waits before its processor is called, as a 
slow lookup or "
+                                                                 + 
"calculation",
                 "step=${step} delay duration=${duration} rate=${rate} 
until=${until}",
                 List.of(step(), new Placeholder("duration", "duration", "The 
time, or a range of times", "1s", "1s", null),
                         rate("0.25"), until("10m"))));
         return answer;
     }
 
+    /**
+     * What a recipe helps to find, when not to use it, and what to look at 
while it runs: to guide a person, or an AI,
+     * that picks a recipe (CAMEL-26699).
+     */
+    record Guide(List<String> uncovers, List<String> avoid, List<String> 
watch) {
+    }
+
+    static Map<String, Guide> guides() {
+        Map<String, Guide> answer = new LinkedHashMap<>();
+        answer.put("http-error", new Guide(
+                List.of(
+                        "An error handler that does not retry, or retries a 
status that will not change, "
+                        + "such as 400 or 404",
+                        "Redeliveries without a delay, which hit an overloaded 
service again at once",
+                        "Messages that are lost instead of reaching the dead 
letter channel"),
+                List.of(
+                        "The endpoint is not HTTP: use Refuses the connection",
+                        "To test a timeout: a status comes back at once, use 
Times out"),
+                List.of(
+                        "The redeliveries of the step and the exchanges 
handled by the error handler",
+                        "The dead letter channel, and a circuit breaker around 
the call if there is one")));
+        answer.put("connection-refused", new Guide(
+                List.of(
+                        "Whether a connection error is retried: that is safe, 
as nothing reached the "
+                        + "other side",
+                        "Long redelivery delays that hold the consumer while 
the service is down",
+                        "A circuit breaker that does not open"),
+                List.of(
+                        "To test an HTTP status from a service that is up: use 
Returns an error"),
+                List.of(
+                        "The redeliveries and the exchanges handled by the 
error handler",
+                        "The state of a circuit breaker around the call")));
+        answer.put("slow", new Guide(
+                List.of(
+                        "A consumer that falls behind: the throughput of the 
route drops and messages "
+                        + "wait in front of it",
+                        "Thread pools that fill up with waiting exchanges",
+                        "Callers of the route (request/reply) that time out"),
+                List.of(
+                        "To test the timeout of the client: the wait is before 
the call, so the client's "
+                        + "timeout does not fire. Use Times out",
+                        "On a route started by a timer: the timer waits for 
the exchange, so there are "
+                                                                  + "fewer 
messages, not more load"),
+                List.of(
+                        "The mean, p95 and p99 time of the routes, and their 
throughput",
+                        "The exchanges in flight")));
+        answer.put("timeout", new Guide(
+                List.of(
+                        "Whether a timeout is retried, and whether that is 
safe: in real life the other "
+                        + "side may have done the work",
+                        "How long a message takes when every attempt times 
out: the wait times the "
+                                                         + "attempts",
+                        "A consumer that stops taking messages while its 
threads wait"),
+                List.of(
+                        "A long wait with a high rate: each fault holds a 
thread for that time"),
+                List.of(
+                        "The time of the route and the exchanges in flight",
+                        "The redeliveries and the dead letter channel")));
+        answer.put("failed-after-success", new Guide(
+                List.of(
+                        "Duplicates: an order booked twice, a payment taken 
twice",
+                        "Whether the other side or the route is idempotent, 
such as with an idempotent "
+                                                                               
     + "consumer or a key the other side checks"),
+                List.of(
+                        "Against a real service where a duplicate costs, such 
as one that charges or "
+                        + "sends mail to people",
+                        "With a high count: until=1 makes one send fail, which 
is enough to see one "
+                                                  + "duplicate"),
+                List.of(
+                        "What the other side received: two of the same",
+                        "The redeliveries of the step")));
+        answer.put("outage", new Guide(
+                List.of(
+                        "A circuit breaker that opens during the outage and 
closes after it",
+                        "Redelivery that gives up before the service is back, 
so the messages go to the "
+                                                                               
               + "dead letter channel",
+                        "How long the route needs to catch up after an 
outage"),
+                List.of(
+                        "An outage longer than all the redeliveries together 
sends every message in it to "
+                        + "the dead letter channel: make sure that is what you 
want to test"),
+                List.of(
+                        "The faults per second: on during the outage, off 
between",
+                        "The state of a circuit breaker, and the exchanges 
handled by the error handler")));
+        answer.put("corrupt-entry", new Guide(
+                List.of(
+                        "Validation that is missing, so a bad message gets 
deep into the route before it "
+                        + "fails",
+                        "A poison message that is redelivered again and again, 
instead of going to the "
+                                   + "dead letter channel",
+                        "Unmarshal and parse errors that are not handled"),
+                List.of(
+                        "When the body is a stream or a file: the charset and 
the cut change text and "
+                        + "bytes only, so those messages are skipped (see the 
reasons it skipped)"),
+                List.of(
+                        "The errors of the steps that read the body",
+                        "The messages the rule skipped, and why")));
+        answer.put("empty-body", new Guide(
+                List.of(
+                        "A service, or the next route, that does not check for 
an empty body",
+                        "An empty file or message that is accepted and passed 
on"),
+                List.of(
+                        "To test the route itself with bad input: use Arrives 
corrupt on the route"),
+                List.of(
+                        "The route or the service that receives it")));
+        answer.put("missing-header", new Guide(
+                List.of(
+                        "A receiver that needs the header and fails without 
it, or worse, goes on and "
+                        + "does the wrong thing",
+                        "Tracing that breaks, as the trace id does not reach 
the next service"),
+                List.of(
+                        "With a header the endpoint does not send on, such as 
most Camel headers: nothing "
+                        + "changes"),
+                List.of(
+                        "The route or the service that receives it")));
+        answer.put("poll-error", new Guide(
+                List.of(
+                        "A consumer without a backoff, which fails every poll 
and fills the log",
+                        "A health check that does not show a consumer that 
cannot poll"),
+                List.of(
+                        "On a consumer that does not poll on a schedule, such 
as a timer, Kafka or JMS: "
+                        + "the rule fires 0 times"),
+                List.of(
+                        "The errors and the backoff of the consumer",
+                        "The log and the health checks")));
+        answer.put("poll-skip", new Guide(
+                List.of(
+                        "Messages that pile up at the source",
+                        "Alerts on a consumer that goes quiet",
+                        "Batching or ordering that counts on every poll"),
+                List.of(
+                        "On a consumer that does not poll on a schedule, such 
as a timer, Kafka or JMS: "
+                        + "the rule fires 0 times"),
+                List.of(
+                        "The polls of the consumer, and the routes after it 
that get nothing")));
+        answer.put("stall", new Guide(
+                List.of(
+                        "Senders that block or fail when the route does not 
take messages, such as direct "
+                        + "callers or a full seda queue",
+                        "Health checks and alerts for a route that stopped",
+                        "How long the route needs to catch up"),
+                List.of(
+                        "On a route others call directly and wait for: they 
fail while it stalls, which "
+                        + "may or may not be the test"),
+                List.of(
+                        "The state of the route",
+                        "The callers, and the queue in front of the route")));
+        answer.put("flap", new Guide(
+                List.of(
+                        "Messages lost or delivered twice across a restart",
+                        "Connections or other resources that leak on each 
start",
+                        "Health checks and alerts that flap with it"),
+                List.of(
+                        "On a route whose consumer takes long to start: make 
the time between longer than "
+                        + "the start"),
+                List.of(
+                        "The state and the uptime of the route",
+                        "The effect: completed against failed or lost 
messages")));
+        answer.put("removed", new Guide(
+                List.of(
+                        "Routes that call it and fail while it is gone",
+                        "State kept in memory that is lost with it, such as an 
aggregator or an "
+                                                                         + 
"idempotent repository"),
+                List.of(
+                        "On a route whose state in memory you want to keep"),
+                List.of(
+                        "The callers of the route",
+                        "Its counters, which start again from zero")));
+        answer.put("dlq-unreachable", new Guide(
+                List.of(
+                        "That the dead letter channel is a single point of 
failure",
+                        "Whether lost messages are noticed: the report counts 
them as lost, and the "
+                                                                               
      + "verdict fails"),
+                List.of(
+                        "On its own: without another rule, or a real error, 
that sends messages to the "
+                        + "dead letter channel, nothing happens"),
+                List.of(
+                        "The messages lost, and the verdict")));
+        answer.put("step-error", new Guide(
+                List.of(
+                        "onException clauses that do not match the exception",
+                        "Redelivery of a step that will fail again: a bug is 
not a temporary error",
+                        "Work done by the steps before it that is not undone"),
+                List.of(
+                        "To test a call to another system: put the fault on 
its endpoint, with Returns an "
+                        + "error or Refuses the connection"),
+                List.of(
+                        "The redeliveries of the step and the exchanges 
handled by the error handler",
+                        "The errors, with the step that failed")));
+        answer.put("step-slow", new Guide(
+                List.of(
+                        "The step that the time of the route comes from",
+                        "Callers of the route that wait for it, and time out"),
+                List.of(
+                        "In a route started by a timer: the timer waits for 
the exchange, so there are "
+                        + "fewer messages, not more load"),
+                List.of(
+                        "The times of the steps and of the route")));
+        return answer;
+    }
+
     private static JsonArray recipes() {
         JsonArray answer = new JsonArray();
+        Map<String, Guide> guides = guides();
         for (Recipe recipe : recipeList()) {
             JsonObject jo = new JsonObject();
             jo.put("id", recipe.id());
             jo.put("group", recipe.group());
             jo.put("title", recipe.title());
             jo.put("description", recipe.description());
+            Guide guide = guides.get(recipe.id());
+            if (guide != null) {
+                jo.put("uncovers", new JsonArray(guide.uncovers()));
+                jo.put("avoid", new JsonArray(guide.avoid()));
+                jo.put("watch", new JsonArray(guide.watch()));
+            }
             jo.put("rule", recipe.rule());
             JsonArray placeholders = new JsonArray();
             for (Placeholder p : recipe.placeholders()) {
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
index 111d5db2d4ff..f14a97807bd3 100644
--- 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
@@ -126,6 +126,21 @@ class ChaosModelTest {
         ChaosRuleParser.parse(fill(recipe, true), new DefaultClassResolver(), 
MAX);
     }
 
+    @Test
+    void everyRecipeSaysWhatItUncoversWhenNotToUseItAndWhatToWatch() {
+        // CAMEL-26699: the add form guides a person (or an AI) with these
+        Map<String, ChaosModel.Guide> guides = ChaosModel.guides();
+        for (Recipe recipe : ChaosModel.recipeList()) {
+            ChaosModel.Guide guide = guides.get(recipe.id());
+            assertThat(guide).as(recipe.id()).isNotNull();
+            assertThat(guide.uncovers()).as(recipe.id()).isNotEmpty();
+            assertThat(guide.avoid()).as(recipe.id()).isNotEmpty();
+            assertThat(guide.watch()).as(recipe.id()).isNotEmpty();
+        }
+        assertThat(guides.keySet()).containsExactlyInAnyOrderElementsOf(
+                ChaosModel.recipeList().stream().map(Recipe::id).toList());
+    }
+
     @Test
     void theModeOfACorruptRecipeOffersEveryMode() {
         Recipe recipe = ChaosModel.recipeList().stream().filter(r -> 
r.id().equals("corrupt-entry")).findFirst()
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
index 6fef3adeb031..0371afbf8252 100644
--- 
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
@@ -345,8 +345,10 @@ final class ChaosRecipeForm {
                 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(),
+            List<Line> lines = new ArrayList<>();
+            lines.add(Line.from(Span.styled(recipe.rule(), Theme.muted())));
+            addGuide(lines, recipe);
+            
frame.renderWidget(Paragraph.builder().overflow(Overflow.WRAP_WORD).text(Text.from(lines)).build(),
                     new Rect(pad.x(), y, pad.width(), Math.max(1, pad.bottom() 
- y)));
             return;
         }
@@ -380,11 +382,33 @@ final class ChaosRecipeForm {
         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())));
+        addGuide(bottom, recipe);
         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)));
     }
 
+    /**
+     * What the recipe can uncover, when not to use it and what to watch while 
it runs, as the model of camel-chaos
+     * says: under the rule, as far as there is room.
+     */
+    static void addGuide(List<Line> lines, ChaosRecipes.Recipe recipe) {
+        addSection(lines, "What it can uncover", recipe.uncovers(), 
Theme.success());
+        addSection(lines, "When not to use it", recipe.avoid(), 
Theme.warning());
+        addSection(lines, "What to watch", recipe.watch(), Theme.info());
+    }
+
+    private static void addSection(List<Line> lines, String title, 
List<String> items, Style style) {
+        if (items == null || items.isEmpty()) {
+            return;
+        }
+        lines.add(Line.empty());
+        lines.add(Line.from(Span.styled(title, style.bold())));
+        for (String item : items) {
+            lines.add(Line.from(Span.styled("• ", style), Span.raw(item)));
+        }
+    }
+
     /** The keys of the form, for the footer. */
     void renderFooter(List<Span> spans) {
         if (inForm) {
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
index 71200da83209..1e78a9f04fb3 100644
--- 
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
@@ -41,7 +41,12 @@ final class ChaosRecipes {
     }
 
     /** A known rule. */
-    record Recipe(String id, String group, String title, String description, 
String rule, List<Placeholder> placeholders) {
+    /**
+     * A known rule. Besides what it does, the model says what it can uncover, 
when not to use it and what to watch
+     * while it runs (CAMEL-26699): empty with a catalog from before that.
+     */
+    record Recipe(String id, String group, String title, String description, 
String rule, List<Placeholder> placeholders,
+            List<String> uncovers, List<String> avoid, List<String> watch) {
 
         /** The rule with the values of the placeholders; a placeholder 
without a value stays {@code ${name}}. */
         String fill(Map<String, String> values) {
@@ -112,7 +117,8 @@ final class ChaosRecipes {
             }
             answer.add(new Recipe(
                     r.getString("id"), r.getStringOrDefault("group", ""), 
r.getString("title"),
-                    r.getString("description"), r.getString("rule"), 
placeholders));
+                    r.getString("description"), r.getString("rule"), 
placeholders,
+                    strings(r.get("uncovers")), strings(r.get("avoid")), 
strings(r.get("watch"))));
         }
         return answer.isEmpty() ? null : new ChaosRecipes(answer);
     }
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 b812000b0429..a830e2ce3931 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
@@ -60,7 +60,9 @@ error handler (HAND) and the redeliveries (REDEL).
 
 - **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,
+  failed", "The route flaps"). Each says what it does, what it can
+  uncover, when not to use it, and what to watch while it runs. 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
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
index 3df8981db10e..d085391eb61b 100644
--- 
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
@@ -82,6 +82,21 @@ class ChaosRecipeFormTest {
                 .contains("endpoint=${endpoint} error status=${status} 
rate=${rate}");
     }
 
+    @Test
+    void theRecipeSaysWhatItUncoversWhenNotToUseItAndWhatToWatch() {
+        key('a');
+        assertThat(screen()).contains("What it can uncover").contains("• An 
error handler that does not retry")
+                .contains("When not to use it").contains("• The endpoint is 
not HTTP: use Refuses the connection")
+                .contains("What to watch").contains("• The redeliveries of the 
step");
+        // and while it is filled in
+        key(KeyCode.ENTER);
+        assertThat(screen()).contains("What it can uncover").contains("Enter 
to add it");
+        // a recipe without a guide shows none
+        key(KeyCode.ESCAPE);
+        key(KeyCode.DOWN);
+        assertThat(screen()).contains("The route stalls").doesNotContain("What 
it can uncover");
+    }
+
     @Test
     void aRecipeFilledWithTheEndpointOfTheIntegrationAndItsDefaults() {
         key('a');
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
index c27656c89d3f..879a3e2cf6e0 100644
--- 
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
@@ -34,6 +34,9 @@ class ChaosRecipesTest {
             {"chaos": {"recipes": [
               {"id": "http-error", "group": "Downstream", "title": "Returns an 
error (HTTP status)",
                "description": "An HTTP call fails with a status",
+               "uncovers": ["An error handler that does not retry"],
+               "avoid": ["The endpoint is not HTTP: use Refuses the 
connection"],
+               "watch": ["The redeliveries of the step"],
                "rule": "endpoint=${endpoint} error status=${status} 
rate=${rate}",
                "placeholders": [
                  {"name": "endpoint", "type": "endpoint", "description": "The 
HTTP endpoint the route calls",
@@ -109,6 +112,11 @@ class ChaosRecipesTest {
         
assertThat(http.placeholders()).extracting(ChaosRecipes.Placeholder::name).containsExactly("endpoint",
 "status",
                 "rate");
         
assertThat(http.placeholders().get(0).schemes()).containsExactly("http", 
"https");
+        // what it can uncover, when not to use it and what to watch 
(CAMEL-26699); none from an older catalog
+        assertThat(http.uncovers()).containsExactly("An error handler that 
does not retry");
+        assertThat(http.avoid()).containsExactly("The endpoint is not HTTP: 
use Refuses the connection");
+        assertThat(http.watch()).containsExactly("The redeliveries of the 
step");
+        assertThat(recipes.recipes().get(1).uncovers()).isEmpty();
         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

Reply via email to