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 af6bef4113d8 CAMEL-26493: camel-jbang - AI tools: a Citrus test is 
checked as YAML, not as a route, and the examples show their Citrus tests 
(#28872)
af6bef4113d8 is described below

commit af6bef4113d853d5595b9ffe9fbbb45005cd97ba
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:25:41 2026 +0100

    CAMEL-26493: camel-jbang - AI tools: a Citrus test is checked as YAML, not 
as a route, and the examples show their Citrus tests (#28872)
    
    * CAMEL-26493: camel-jbang - AI tools: a Citrus test is checked as YAML, 
not as a route, and the examples show their Citrus tests
    
    The local-model benchmark example test-citrus-orders asks for a Citrus test 
in
    test/orders.citrus.it.yaml, run with camel test run. It showed two gaps.
    
    camel_write_file, camel_edit_file and camel_validate_source checked the 
test as a
    Camel YAML route. The first test of the model was refused, as a Citrus 
variable
    (${orderId}) was read as a Simple function. The next one was written with 
the
    problem "a Camel YAML file is a list of entries", and the answer said that 
the
    running integration reloaded the file. The model then made the test a list, 
and
    then a route.
    
    camel_catalog_examples had no Citrus test to show: a search for citrus 
found no
    example, and the files of an example leave out test/.
    
    Now:
    - A Citrus test (name.citrus.yaml, name.citrus.it.yaml, any 
name.citrus.*.yaml) is
      checked for its YAML syntax only: a parse error refuses the write, with 
its line.
      The Camel YAML repairs and the route notes are not applied to it.
    - The write of a Citrus test does not wait for a reload and does not report 
one.
      camel run --dev --source-dir reloads the routes when the test changes, 
but it
      loads no route from the test. The answer says to run the test with camel 
test run.
    - The example tools list the Citrus test of an example 
(test/<name>.citrus.it.yaml)
      with its files, and read it. The tests of the bundled examples are 
bundled, and
      the sync of the example catalog fetches them. camel run --example still 
runs only
      the files of the catalog.
    - A search for citrus finds the examples with a Citrus test. A filter finds 
the
      examples that have every word of it, so "csv-to-json test" finds that 
example.
    - The MCP server instructions say where to find a sample Citrus test.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_016Y4dDi5iQNgtHrDdz2NpwF
    
    * CAMEL-26493: camel-jbang - ExampleRoutesLoadTest leaves out the Citrus 
tests of the examples, they are not routes
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 dsl/camel-jbang/camel-jbang-core/pom.xml           |   5 +-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  47 +++--
 .../jbang/core/commands/ai/SourceValidator.java    |  59 +++++-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |  29 ++-
 .../camel/dsl/jbang/core/common/ExampleHelper.java |  60 +++---
 .../test/file-processing.citrus.it.yaml            |  26 +++
 .../http-client/test/http-client.citrus.it.yaml    |  21 ++
 .../stock-api/test/stock-api.citrus.it.yaml        |  49 +++++
 .../test/openapi-client.citrus.it.yaml             |  39 ++++
 .../test/openapi-server.citrus.it.yaml             |  76 +++++++
 .../test/rest-code-first.citrus.it.yaml            |  95 +++++++++
 .../test/circuit-breaker.citrus.it.yaml            |  26 +++
 .../test/error-handling.citrus.it.yaml             |  36 ++++
 .../rest-api/test/rest-api.citrus.it.yaml          |  40 ++++
 .../quick-start/routes/test/routes.citrus.it.yaml  |  19 ++
 .../splitter/test/splitter.citrus.it.yaml          |  19 ++
 .../timer-log/test/timer-log.citrus.it.yaml        |  16 ++
 .../aggregator/test/aggregator.citrus.it.yaml      |  25 +++
 .../test/content-based-router.citrus.it.yaml       |  30 +++
 .../test/dynamic-routing.citrus.it.yaml            |  35 ++++
 .../test/enrich-and-dedupe.citrus.it.yaml          |  34 ++++
 .../test/filter-and-multicast.citrus.it.yaml       |  30 +++
 .../order-lines/test/order-lines.citrus.it.yaml    |  25 +++
 .../test/nightly-report.citrus.it.yaml             |  16 ++
 .../test/order-generator.citrus.it.yaml            |  24 +++
 .../test/properties-and-profiles.citrus.it.yaml    |  16 ++
 .../csv-to-json/test/csv-to-json.citrus.it.yaml    |  21 ++
 .../data-mapping/test/data-mapping.citrus.it.yaml  |  24 +++
 .../transform/groovy/test/groovy.citrus.it.yaml    |  24 +++
 .../test/json-transform.citrus.it.yaml             |  23 +++
 .../xml-to-json/test/xml-to-json.citrus.it.yaml    |  16 ++
 .../transform/xslt/test/xslt.citrus.it.yaml        |  23 +++
 .../core/commands/ai/AuthoringToolsCitrusTest.java | 221 +++++++++++++++++++++
 .../core/commands/ai/ExampleToolsRegistryTest.java |  54 ++++-
 .../dsl/jbang/core/common/ExampleHelperTest.java   |  29 +++
 .../jbang/core/common/ExampleRoutesLoadTest.java   |   6 +-
 .../dsl/jbang/core/commands/mcp/ExampleTools.java  |   9 +-
 .../core/commands/mcp/McpServerInstructions.java   |   2 +
 .../jbang/core/commands/mcp/ExampleToolsTest.java  |  16 ++
 .../commands/mcp/McpServerInstructionsTest.java    |   7 +
 40 files changed, 1321 insertions(+), 51 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml 
b/dsl/camel-jbang/camel-jbang-core/pom.xml
index b1a5b666c0d2..d87c1d528a73 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -527,7 +527,10 @@
                                             }
                                             files.findAll { it.directory 
}.each { it.deleteDir() }
                                             catalog.findAll { it.bundled 
}.each { ex ->
-                                                ex.files.each { f ->
+                                                // the files it runs with, and 
its Citrus test (test/<name>.citrus.it.yaml), which the catalog does not list
+                                                def leaf = 
ex.name.substring(ex.name.lastIndexOf('/') + 1)
+                                                def fetched = ex.files + 
(ex.hasCitrusTests ? ['test/' + leaf + '.citrus.it.yaml'] : [])
+                                                fetched.each { f ->
                                                     def target = new 
File(dest, ex.name + '/' + f)
                                                     target.parentFile.mkdirs()
                                                     try {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 8bf33e1dd72d..d62d000be043 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -660,12 +660,25 @@ public final class AuthoringTools {
                 result.put("notChecked", notChecked);
             }
         }
-        result.put("message", errors.isEmpty()
-                ? "The source is valid"
-                : errors.size() + " problem(s) found; fix them before writing 
the file");
+        if (!errors.isEmpty()) {
+            result.put("message", errors.size() + " problem(s) found; fix them 
before writing the file");
+        } else if (SourceValidator.isCitrusTest(file)) {
+            result.put("message", "The YAML is valid. " + 
citrusTestNote(file));
+        } else {
+            result.put("message", "The source is valid");
+        }
         return result;
     }
 
+    /**
+     * What the tools say of a Citrus test: it is not a route, so only its 
YAML syntax is checked, and there is no
+     * reload to wait for or to report (camel run --dev --source-dir loads no 
routes from it); camel test run runs it.
+     */
+    static String citrusTestNote(String file) {
+        return "A Citrus test is not a route: only its YAML syntax is checked, 
and no integration loads it. Run it with"
+               + " camel test run " + file + " in the project directory.";
+    }
+
     /** The line of a validation message, "Line N: ...", or 0. */
     private static int lineOf(String error) {
         if (error == null || !error.startsWith("Line ")) {
@@ -723,9 +736,11 @@ public final class AuthoringTools {
         }
         if ("unchanged".equals(result.getString("status"))) {
             result.put("message", "The edit leaves the file as it is (the 
replacement gives the text the file already"
-                                  + " has), so nothing was written and nothing 
reloads. To change what the route does,"
-                                  + " change the text; camel_get_log and 
camel_get_errors show what the current version"
-                                  + " does.");
+                                  + " has), so nothing was written and nothing 
reloads. "
+                                  + (SourceValidator.isCitrusTest(file)
+                                          ? citrusTestNote(file)
+                                          : "To change what the route does, 
change the text; camel_get_log and"
+                                            + " camel_get_errors show what the 
current version does."));
         } else if (!"invalid".equals(result.getString("status"))) {
             result.put("status", "edited");
             if (edit.get("matchedLines") instanceof JsonArray matched) {
@@ -1524,7 +1539,7 @@ public final class AuthoringTools {
             if (!notes.isEmpty()) {
                 result.put("notes", new JsonArray(notes));
             }
-        } else if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+        } else if ((name.endsWith(".yaml") || name.endsWith(".yml")) && 
!SourceValidator.isCitrusTest(name)) {
             // a JMS reply header set by hand, with nothing sent InOut; a to: 
that copies the file a consumer moves
             List<String> notes = new 
ArrayList<>(HeaderChecks.replyHeaderNotes(content));
             notes.addAll(WriteNotes.fileCopyNotes(content));
@@ -1680,6 +1695,8 @@ public final class AuthoringTools {
         List<String> problemsBefore = List.of();
         List<String> repaired = List.of();
         List<String> repairable = List.of();
+        // a Citrus test is not a route: none of the repairs, notes and 
reloads of a route apply to it
+        boolean citrusTest = SourceValidator.isCitrusTest(file);
         if (validate && SourceValidator.isValidatableFile(file)) {
             // a missing consumer of a direct: endpoint does not refuse the 
write: it is often a file not written yet
             List<String> errors = SourceValidator.validate(file, content, 
ctx.catalog(), ctx.propertyLineValidator(), dir,
@@ -1694,7 +1711,7 @@ public final class AuthoringTools {
                 errors = newProblems(before, errors, stillThere);
                 problemsBefore = stillThere;
             }
-            if (!errors.isEmpty()) {
+            if (!errors.isEmpty() && !citrusTest) {
                 // CAMEL-25532: the shapes with one reading are repaired, and 
the write goes ahead when that leaves no
                 // new problem; the answer says what was repaired
                 Repaired r = repair(ctx, dir, file, content, errors, before, 
exists);
@@ -1744,8 +1761,10 @@ public final class AuthoringTools {
             result.put("file", file);
             result.put("directory", dir.toString());
             result.put("message", "The file already has exactly this content, 
so nothing was written and nothing"
-                                  + " reloads. To change what the route does, 
change the content; camel_get_log and"
-                                  + " camel_get_errors show what the current 
version does.");
+                                  + " reloads. " + (citrusTest
+                                          ? citrusTestNote(file)
+                                          : "To change what the route does, 
change the content; camel_get_log and"
+                                            + " camel_get_errors show what the 
current version does."));
             return result;
         }
         // a client that asks before each write (camel ask, CAMEL-25676) shows 
the change first; the others write as
@@ -1759,7 +1778,9 @@ public final class AuthoringTools {
         String processName = null;
         String sinceKey = null;
         long watchPid = -1;
-        if (SourceValidator.isValidatableFile(file) || 
reloadsWithRoutes(file)) {
+        // not for a Citrus test: dev mode with --source-dir reloads the 
routes when it changes, but loads no route from
+        // it, and the answer said the integration reloaded a file that it 
does not load (test-citrus-orders)
+        if (!citrusTest && (SourceValidator.isValidatableFile(file) || 
reloadsWithRoutes(file))) {
             if (ctx.hasProcess()) {
                 watchPid = ctx.pid();
                 RuntimeHelper.ProcessInfo p = 
RuntimeHelper.findProcess(Long.toString(ctx.pid()));
@@ -1809,7 +1830,7 @@ public final class AuthoringTools {
         boolean replyByHand = false;
         boolean copies = false;
         List<String> unusedProperties = List.of();
-        if (validate) {
+        if (validate && !citrusTest) {
             putFileNotes(result, file, content);
             String lower = file.toLowerCase(Locale.ROOT);
             boolean yaml = lower.endsWith(".yaml") || lower.endsWith(".yml");
@@ -1861,6 +1882,8 @@ public final class AuthoringTools {
                                  + " content and write again (see 
reload.message).";
                 default -> "Written; " + reload.getString("message");
             });
+        } else if (citrusTest) {
+            result.put("message", citrusTestNote(file));
         } else if (notWatchedBy >= 0) {
             result.put("message", "The running integration (pid " + 
notWatchedBy + ") was started with its own files"
                                   + " and does not reload when this file 
changes; camel_control restart picks up the"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 7e4bfc366be5..a9b14e74e2fb 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -17,10 +17,12 @@
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.io.IOException;
+import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -40,6 +42,13 @@ import org.apache.camel.dsl.yaml.validator.ProjectFacts;
 import org.apache.camel.dsl.yaml.validator.YamlPointerLines;
 import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import org.snakeyaml.engine.v2.nodes.Node;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.Mark;
+import org.yaml.snakeyaml.error.MarkedYAMLException;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.events.Event;
 
 import static 
org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.JAVA_CLASS_PATTERN;
 import static 
org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.JAVA_PACKAGE_PATTERN;
@@ -69,9 +78,20 @@ public final class SourceValidator {
                 || name.endsWith(".java") || name.endsWith(".xsl") || 
name.endsWith(".xslt") || name.endsWith(".xml");
     }
 
+    /** The names Citrus runs as a YAML test: name.citrus.yaml, 
name.citrus.it.yaml, name.citrus-test.yaml... */
+    private static final Pattern CITRUS_TEST = 
Pattern.compile(".+\\.citrus(?:[.-][^/\\\\]+)?\\.ya?ml");
+
+    /**
+     * Whether the file is a Citrus test, such as test/orders.citrus.it.yaml: 
camel test run runs it, and no integration
+     * loads it. It is YAML of its own, not a Camel route, so only its YAML 
syntax is checked.
+     */
+    public static boolean isCitrusTest(String fileName) {
+        return fileName != null && 
CITRUS_TEST.matcher(fileName.toLowerCase(Locale.ROOT)).matches();
+    }
+
     /**
-     * Validates source by file type: Camel YAML DSL for .yaml/.yml files, 
Camel options for .properties files. Other
-     * file types have no validation and yield no messages.
+     * Validates source by file type: Camel YAML DSL for .yaml/.yml files (a 
Citrus test for its YAML syntax only),
+     * Camel options for .properties files. Other file types have no 
validation and yield no messages.
      *
      * @param  fileName          the file name; its extension picks the checks
      * @param  content           the source
@@ -117,6 +137,11 @@ public final class SourceValidator {
             Path directory, YamlValidator schemaValidator, boolean 
checkConsumers) {
         Objects.requireNonNull(catalog, "catalog");
         String name = fileName == null ? "" : 
fileName.toLowerCase(Locale.ROOT);
+        if (isCitrusTest(name)) {
+            // a Citrus test is not a route: checked as one, a new test was 
refused ("a Camel YAML file is a list of
+            // entries"), and a local model then wrote a route in its place 
(test-citrus-orders of the benchmark)
+            return validateYamlSyntax(content);
+        }
         if (name.endsWith(".yaml") || name.endsWith(".yml")) {
             // a rest binding to an OpenAPI specification hides the verb, and 
the specification is a file beside the
             // route: read it, so that a GET operation is known to carry no 
body (CAMEL-24844), and with the REST binding
@@ -617,6 +642,36 @@ public final class SourceValidator {
         return XmlChecks.validateXml(content);
     }
 
+    /**
+     * The YAML syntax error of a YAML file that is not a route, such as a 
Citrus test, with its line: the parser stops
+     * at the first one. Empty when the content parses.
+     */
+    static List<String> validateYamlSyntax(String content) {
+        if (content == null || content.isBlank()) {
+            return List.of();
+        }
+        try {
+            Iterator<Event> events = new Yaml(new SafeConstructor(new 
LoaderOptions())).parse(new StringReader(content))
+                    .iterator();
+            while (events.hasNext()) {
+                events.next();
+            }
+            return List.of();
+        } catch (MarkedYAMLException e) {
+            Mark at = e.getProblemMark();
+            Mark from = e.getContextMark();
+            // where the parser was reading from, such as the line that opened 
a quote that never closes
+            String context = e.getContext() != null && from != null
+                    ? " (" + e.getContext() + " that starts at line " + 
(from.getLine() + 1) + ")" : "";
+            return List.of(at != null
+                    ? "Line " + (at.getLine() + 1) + ": the YAML does not 
parse at column " + (at.getColumn() + 1) + ": "
+                      + e.getProblem() + context
+                    : "The YAML does not parse: " + e.getProblem() + context);
+        } catch (YAMLException e) {
+            return List.of("The YAML does not parse: " + e.getMessage());
+        }
+    }
+
     /**
      * A Camel* header that no component used in the file defines 
(CamelTimerIndex; the timer sets CamelTimerCounter):
      * the value is null at runtime. Checked against the header metadata of 
every component the file names, with the
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index ea135305d2e7..b6624e5d0dc1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -1147,8 +1147,12 @@ public final class ToolRegistry {
             = "Example name as listed, e.g. quick-start/timer-log or 
fail-well/circuit-breaker (the short name without "
               + "the group, such as timer-log, works too)";
     public static final String EXAMPLE_FILE_DESC
-            = "File name within the example, as in its files, e.g. 
timer-log.camel.yaml or application.properties "
-              + "for quick-start/timer-log";
+            = "File name within the example, as in its files, e.g. 
timer-log.camel.yaml, application.properties "
+              + "or test/timer-log.citrus.it.yaml (its Citrus test) for 
quick-start/timer-log";
+    /** The filter of the example list, shared with the MCP server. */
+    public static final String EXAMPLE_FILTER_DESC
+            = "Filter by name, description or tag (case-insensitive; every 
word must match); citrus finds the "
+              + "examples with a Citrus test";
 
     private static void registerExampleTools() {
         register(tool("list_examples",
@@ -1156,10 +1160,10 @@ public final class ToolRegistry {
                                        + "route, fail-well, connect, 
connect-service, contracts, ai, cloud, showcase). "
                                        + "Returns the groups (level, title, 
intro) and the examples in reading order with "
                                        + "name, title, description, level, 
order, tags, what they teach, the infra "
-                                       + "services they need, whether they are 
bundled and their files. "
+                                       + "services they need, whether they are 
bundled and their files (test/ has the "
+                                       + "Citrus test, run with camel test 
run). "
                                        + "Call it without arguments for the 
whole ladder, with level for one group.")
-                .param("filter", "string",
-                        "Filter by name, description, or tag 
(case-insensitive)", false)
+                .param("filter", "string", EXAMPLE_FILTER_DESC, false)
                 .param("level", "string", EXAMPLE_LEVEL_DESC, false)
                 .param("limit", "integer", "Maximum number of examples to 
return (default: 50)", false)
                 .executor((ctx, args) -> {
@@ -1224,7 +1228,7 @@ public final class ToolRegistry {
                             }
                             jo.put("bundled", ExampleHelper.isBundled(entry));
                             jo.put("requiresDocker", 
ExampleHelper.requiresDocker(entry));
-                            jo.put("files", ExampleHelper.getFiles(entry));
+                            jo.put("files", exampleFiles(entry));
                             results.add(jo);
                         }
                     }
@@ -1259,7 +1263,7 @@ public final class ToolRegistry {
         String fileName = file.trim();
         JsonObject entry = resolveExample(ExampleHelper.loadCatalog(), wanted);
         String name = entry.getString("name");
-        List<String> files = ExampleHelper.getFiles(entry);
+        List<String> files = exampleFiles(entry);
         if (!files.contains(fileName)) {
             throw new ToolExecutionException(
                     "File '" + fileName + "' not found in example '" + name + 
"'. Available files: " + files);
@@ -1284,6 +1288,17 @@ public final class ToolRegistry {
         return answer;
     }
 
+    /**
+     * The files of an example as the example tools list them: the files it 
runs with, and its Citrus test, which the
+     * catalog leaves out. A model asked to write a Citrus test had no sample 
to read (test-citrus-orders of the
+     * benchmark).
+     */
+    static List<String> exampleFiles(JsonObject entry) {
+        List<String> files = new ArrayList<>(ExampleHelper.getFiles(entry));
+        files.addAll(ExampleHelper.getTestFiles(entry));
+        return files;
+    }
+
     /**
      * The catalog entry of an example by its full name, or by its short name 
when only one example has it, as camel run
      * resolves it; an ambiguous short name answers with the candidates, an 
unknown one with the closest names.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
index 97bdf84b2097..382c4b2a335e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
@@ -141,47 +141,48 @@ public final class ExampleHelper {
         return names;
     }
 
+    /**
+     * The examples whose name, title, description, level or tags have every 
word of the filter (case-insensitive), each
+     * word in any of them: "csv-to-json test" finds the example and its test. 
An example with a Citrus test is found by
+     * citrus, test and its test file as well.
+     */
     public static List<JsonObject> filterExamples(List<JsonObject> catalog, 
String filter) {
-        if (filter == null || filter.isEmpty()) {
+        if (filter == null || filter.isBlank()) {
             return catalog;
         }
-        String lowerFilter = filter.toLowerCase();
+        List<String> words = 
List.of(filter.toLowerCase(Locale.ROOT).trim().split("\\s+"));
         List<JsonObject> result = new ArrayList<>();
         for (JsonObject entry : catalog) {
-            if (matches(entry, lowerFilter)) {
+            List<String> texts = searchTexts(entry);
+            if (words.stream().allMatch(w -> texts.stream().anyMatch(t -> 
t.contains(w)))) {
                 result.add(entry);
             }
         }
         return result;
     }
 
+    /** What a filter searches in an example, in lower case. */
     @SuppressWarnings("unchecked")
-    private static boolean matches(JsonObject entry, String filter) {
-        String name = entry.getString("name");
-        if (name != null && name.toLowerCase().contains(filter)) {
-            return true;
-        }
-        String title = entry.getString("title");
-        if (title != null && title.toLowerCase().contains(filter)) {
-            return true;
-        }
-        String desc = entry.getString("description");
-        if (desc != null && desc.toLowerCase().contains(filter)) {
-            return true;
-        }
-        String level = entry.getString("level");
-        if (level != null && level.toLowerCase().contains(filter)) {
-            return true;
+    private static List<String> searchTexts(JsonObject entry) {
+        List<String> texts = new ArrayList<>();
+        for (String key : new String[] { "name", "title", "description", 
"level" }) {
+            String text = entry.getString(key);
+            if (text != null) {
+                texts.add(text.toLowerCase(Locale.ROOT));
+            }
         }
         Collection<String> tags = (Collection<String>) entry.get("tags");
         if (tags != null) {
             for (String tag : tags) {
-                if (tag.toLowerCase().contains(filter)) {
-                    return true;
-                }
+                texts.add(tag.toLowerCase(Locale.ROOT));
             }
         }
-        return false;
+        if (hasCitrusTests(entry)) {
+            // the catalog has a flag for the test, not its file: a search for 
citrus found no example
+            texts.add("citrus tests");
+            texts.addAll(getTestFiles(entry));
+        }
+        return texts;
     }
 
     /**
@@ -451,6 +452,19 @@ public final class ExampleHelper {
         return citrus != null && citrus;
     }
 
+    /**
+     * The Citrus test of an example, run with camel test run: 
test/name.citrus.it.yaml, the name camel-jbang-examples
+     * gives it (its AGENTS.md). Empty for an example without one. The files 
of the catalog are those the example runs
+     * with, so they leave out its test.
+     */
+    public static List<String> getTestFiles(JsonObject entry) {
+        String name = entry.getString("name");
+        if (!hasCitrusTests(entry) || name == null) {
+            return List.of();
+        }
+        return List.of("test/" + name.substring(name.lastIndexOf('/') + 1) + 
".citrus.it.yaml");
+    }
+
     /**
      * What an example needs beyond its infra services, for the user to do 
before the run (a model to pull, an API key
      * to set, another example to run first), as plain text; null when it 
needs nothing else.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/test/file-processing.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/test/file-processing.citrus.it.yaml
new file mode 100644
index 000000000000..00f2285e0a60
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/test/file-processing.citrus.it.yaml
@@ -0,0 +1,26 @@
+name: file-processing-test
+description: The file-processing example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "file-processing"
+            file: "../file-processing.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "file-processing"
+          logMessage: "Invoice INV-2001 for ORD-1001: 49.4 EUR"
+  - camel:
+      jbang:
+        verify:
+          integration: "file-processing"
+          logMessage: "Invoice invoice-2003.json rejected"
+  - camel:
+      jbang:
+        verify:
+          integration: "file-processing"
+          logMessage: "Invoice INV-2004 for ORD-1003: 53.45 EUR"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/http-client/test/http-client.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/http-client/test/http-client.citrus.it.yaml
new file mode 100644
index 000000000000..1d40c02c003a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/http-client/test/http-client.citrus.it.yaml
@@ -0,0 +1,21 @@
+name: http-client-test
+description: The http-client example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "http-client"
+            file: "../http-client.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "http-client"
+          logMessage: "ORD-1001: CAMEL-TSHIRT x 2, 120 in stock, ok"
+  - camel:
+      jbang:
+        verify:
+          integration: "http-client"
+          logMessage: "ORD-1003: CAMEL-CAP x 1, only 0 in stock, back-order"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/stock-api/test/stock-api.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/stock-api/test/stock-api.citrus.it.yaml
new file mode 100644
index 000000000000..7d653d229739
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/stock-api/test/stock-api.citrus.it.yaml
@@ -0,0 +1,49 @@
+name: stock-api-test
+description: The stock API answers one SKU as JSON and 404 for an unknown one
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "stock-api"
+            file: "../stock-api.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "stock.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "stock-api"
+          logMessage: "(stock-api) started"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/stock/CAMEL-MUG"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+          contentType: "application/json"
+        validate:
+          - jsonPath:
+              - expression: "$.sku"
+                value: "CAMEL-MUG"
+              - expression: "$.qty"
+                value: "42"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/stock/CAMEL-SOCKS"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "404"
+        validate:
+          - jsonPath:
+              - expression: "$.error"
+                value: "unknown sku CAMEL-SOCKS"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-client/test/openapi-client.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-client/test/openapi-client.citrus.it.yaml
new file mode 100644
index 000000000000..69fdb646d8c0
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-client/test/openapi-client.citrus.it.yaml
@@ -0,0 +1,39 @@
+name: openapi-client-test
+description: The client reserves stock by contract against the openapi-server 
example
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "openapi-server"
+            file: "../../openapi-server/openapi-server.camel.yaml"
+            systemProperties:
+              file: "../../openapi-server/application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-server"
+          logMessage: "(openapi-server) started"
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "openapi-client"
+            file: "../openapi-client.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "stock-api.json"
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-client"
+          logMessage: "ORD-1002: reserved 3 x CAMEL-MUG, 42 left on the shelf"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-client"
+          logMessage: "ORD-1003: CAMEL-CAP not reserved, the stock API 
answered 409"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/test/openapi-server.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/test/openapi-server.citrus.it.yaml
new file mode 100644
index 000000000000..6c3449edc783
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/test/openapi-server.citrus.it.yaml
@@ -0,0 +1,76 @@
+name: openapi-server-test
+description: The stock API answers by contract
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "openapi-server"
+            file: "../openapi-server.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "stock-api.json"
+            - "stock.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "openapi-server"
+          logMessage: "(openapi-server) started"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/api/stock/CAMEL-MUG"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+        validate:
+          - jsonPath:
+              - expression: "$.qty"
+                value: "42"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/api/stock/CAMEL-MUG/reserve"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1001", "qty": 2}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+        validate:
+          - jsonPath:
+              - expression: "$.reserved"
+                value: "2"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/api/stock/CAMEL-CAP/reserve"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1003", "qty": 1}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "409"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/api/stock/CAMEL-MUG/reserve"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1001"}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "400"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/test/rest-code-first.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/test/rest-code-first.citrus.it.yaml
new file mode 100644
index 000000000000..b1d7cb212c97
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/test/rest-code-first.citrus.it.yaml
@@ -0,0 +1,95 @@
+name: rest-code-first-test
+description: The order API answers as the README shows
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "rest-code-first"
+            file: "../rest-code-first.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "Order.java"
+            - "orders.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "rest-code-first"
+          logMessage: "(rest-code-first) started"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/orders/ORD-1002"
+          # the client's default Content-Type text/plain makes the json 
binding skip writing the answer
+          contentType: "application/json"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+        validate:
+          - jsonPath:
+              - expression: "$.customer"
+                value: "C-207"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/orders/ORD-9999"
+          # the client's default Content-Type text/plain makes the json 
binding skip writing the answer
+          contentType: "application/json"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "404"
+        validate:
+          - jsonPath:
+              - expression: "$.error"
+                value: "unknown order ORD-9999"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/orders"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1004", "customer": "C-482", "country": 
"DK", "status": "paid", "lines": [{"sku": "CAMEL-MUG", "qty": 1, "price": 
9.5}]}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "201"
+        validate:
+          - jsonPath:
+              - expression: "$.orderId"
+                value: "ORD-1004"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        POST:
+          path: "/orders"
+          contentType: "application/json"
+          body:
+            data: '{"orderId": "ORD-1005", "customer": "C-482", "country": 
"DK", "status": "paid", "lines": []}'
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "400"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/openapi"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+        validate:
+          - jsonPath:
+              - expression: "$.paths['/orders/{id}'].get.operationId"
+                value: "getOrder"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/test/circuit-breaker.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/test/circuit-breaker.citrus.it.yaml
new file mode 100644
index 000000000000..068b417252d5
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/test/circuit-breaker.citrus.it.yaml
@@ -0,0 +1,26 @@
+name: circuit-breaker-test
+description: The circuit-breaker example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "circuit-breaker"
+            file: "../circuit-breaker.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "circuit-breaker"
+          logMessage: "(breaker OPEN): supplier failed, using last known stock"
+  - camel:
+      jbang:
+        verify:
+          integration: "circuit-breaker"
+          logMessage: "(breaker OPEN): breaker open, supplier not called, 
using last known stock"
+  - camel:
+      jbang:
+        verify:
+          integration: "circuit-breaker"
+          logMessage: "Stock check 18 (breaker CLOSED): CAMEL-MUG"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/test/error-handling.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/test/error-handling.citrus.it.yaml
new file mode 100644
index 000000000000..f6ab5fe607f1
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/test/error-handling.citrus.it.yaml
@@ -0,0 +1,36 @@
+name: error-handling-test
+description: The error-handling example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "error-handling"
+            file: "../error-handling.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "parked.camel.yaml"
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "error-handling"
+          logMessage: "Payment for ORD-1001 charged"
+  - camel:
+      jbang:
+        verify:
+          integration: "error-handling"
+          logMessage: "Payment for ORD-1002 charged"
+  - camel:
+      jbang:
+        verify:
+          integration: "error-handling"
+          logMessage: "Payment for ORD-1003 declined: card declined"
+  - camel:
+      jbang:
+        verify:
+          integration: "error-handling"
+          logMessage: "Order ORD-1003 parked for manual review"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/rest-api/test/rest-api.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/rest-api/test/rest-api.citrus.it.yaml
new file mode 100644
index 000000000000..160d16f68fba
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/rest-api/test/rest-api.citrus.it.yaml
@@ -0,0 +1,40 @@
+name: rest-api-test
+description: The REST API answers the greeting, with and without a name
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "rest-api"
+            file: "../rest-api.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "rest-api"
+          logMessage: "(rest-api) started"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/api/hello"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+          body:
+            data: "Hello from Camel REST API!"
+  - http:
+      client: "http://localhost:8080";
+      sendRequest:
+        GET:
+          path: "/api/hello/World"
+  - http:
+      client: "http://localhost:8080";
+      receiveResponse:
+        response:
+          status: "200"
+          body:
+            data: "Hello World from Camel REST API!"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/routes/test/routes.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/routes/test/routes.citrus.it.yaml
new file mode 100644
index 000000000000..b2f129e008d5
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/routes/test/routes.citrus.it.yaml
@@ -0,0 +1,19 @@
+name: routes-test
+description: The route logs the greeting the Greeter bean built
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "routes"
+            file: "../routes.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "beans.yaml"
+            - "Greeter.java"
+  - camel:
+      jbang:
+        verify:
+          integration: "routes"
+          logMessage: "Hello, Camel!"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/splitter/test/splitter.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/splitter/test/splitter.citrus.it.yaml
new file mode 100644
index 000000000000..d3ebde909d83
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/splitter/test/splitter.citrus.it.yaml
@@ -0,0 +1,19 @@
+name: splitter-test
+description: The splitter logs the batch and then one line per item
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "splitter"
+            file: "../splitter.camel.yaml"
+  - camel:
+      jbang:
+        verify:
+          integration: "splitter"
+          logMessage: "Received batch: Laptop,Phone,Tablet,Monitor,Keyboard"
+  - camel:
+      jbang:
+        verify:
+          integration: "splitter"
+          logMessage: "Processing item 4: Keyboard"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/timer-log/test/timer-log.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/timer-log/test/timer-log.citrus.it.yaml
new file mode 100644
index 000000000000..d2b2ebe41c0c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/timer-log/test/timer-log.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: timer-log-test
+description: The timer route logs the greeting from application.properties
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "timer-log"
+            file: "../timer-log.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "timer-log"
+          logMessage: "Hello Camel!"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/aggregator/test/aggregator.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/aggregator/test/aggregator.citrus.it.yaml
new file mode 100644
index 000000000000..9d7e783f8ae2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/aggregator/test/aggregator.citrus.it.yaml
@@ -0,0 +1,25 @@
+name: aggregator-test
+description: The aggregator example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "aggregator"
+            file: "../aggregator.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "aggregator"
+          logMessage: "Shipment for ORD-1001 complete"
+  - camel:
+      jbang:
+        verify:
+          integration: "aggregator"
+          logMessage: "Shipment for ORD-1003 complete"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/content-based-router/test/content-based-router.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/content-based-router/test/content-based-router.citrus.it.yaml
new file mode 100644
index 000000000000..508c9013789e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/content-based-router/test/content-based-router.citrus.it.yaml
@@ -0,0 +1,30 @@
+name: content-based-router-test
+description: The content-based-router example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "content-based-router"
+            file: "../content-based-router.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "content-based-router"
+          logMessage: "ORD-1001 from DK: local delivery"
+  - camel:
+      jbang:
+        verify:
+          integration: "content-based-router"
+          logMessage: "ORD-1002 from DE: EU shipping"
+  - camel:
+      jbang:
+        verify:
+          integration: "content-based-router"
+          logMessage: "ORD-1003 from US: export"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
new file mode 100644
index 000000000000..4794852fa00f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
@@ -0,0 +1,35 @@
+name: dynamic-routing-test
+description: The dynamic-routing example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "dynamic-routing"
+            file: "../dynamic-routing.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Warehouse DK picks ORD-1001"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Invoice for ORD-1001 via the backup service"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Warehouse US picks ORD-1003"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "No invoice for ORD-1003 until it is paid"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/test/enrich-and-dedupe.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/test/enrich-and-dedupe.citrus.it.yaml
new file mode 100644
index 000000000000..b331d2b608df
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/test/enrich-and-dedupe.citrus.it.yaml
@@ -0,0 +1,34 @@
+name: enrich-and-dedupe-test
+description: The enrich-and-dedupe example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "enrich-and-dedupe"
+            file: "../enrich-and-dedupe.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+            - "orders/resent-order-1001.json"
+            - "customers/C-482.json"
+            - "customers/C-207.json"
+            - "customers/C-134.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "enrich-and-dedupe"
+          logMessage: "Order ORD-1001 for Anna Jensen (gold)"
+  - camel:
+      jbang:
+        verify:
+          integration: "enrich-and-dedupe"
+          logMessage: "Order ORD-1001 is a duplicate, skipped"
+  - camel:
+      jbang:
+        verify:
+          integration: "enrich-and-dedupe"
+          logMessage: "Audited ORD-1003.json"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/filter-and-multicast/test/filter-and-multicast.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/filter-and-multicast/test/filter-and-multicast.citrus.it.yaml
new file mode 100644
index 000000000000..dae997f2b82c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/filter-and-multicast/test/filter-and-multicast.citrus.it.yaml
@@ -0,0 +1,30 @@
+name: filter-and-multicast-test
+description: The filter-and-multicast example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "filter-and-multicast"
+            file: "../filter-and-multicast.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "filter-and-multicast"
+          logMessage: "Warehouse: pick 2 line(s) for ORD-1001"
+  - camel:
+      jbang:
+        verify:
+          integration: "filter-and-multicast"
+          logMessage: "Invoicing: bill customer C-207 for ORD-1002"
+  - camel:
+      jbang:
+        verify:
+          integration: "filter-and-multicast"
+          logMessage: "Order ORD-1003 received, status pending"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/order-lines/test/order-lines.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/order-lines/test/order-lines.citrus.it.yaml
new file mode 100644
index 000000000000..6bbfb544fc89
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/order-lines/test/order-lines.citrus.it.yaml
@@ -0,0 +1,25 @@
+name: order-lines-test
+description: The order-lines example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "order-lines"
+            file: "../order-lines.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "order-lines"
+          logMessage: "pick 2 x CAMEL-TSHIRT for ORD-1001"
+  - camel:
+      jbang:
+        verify:
+          integration: "order-lines"
+          logMessage: "Order ORD-1003: all 3 line(s) sent to picking"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/nightly-report/test/nightly-report.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/nightly-report/test/nightly-report.citrus.it.yaml
new file mode 100644
index 000000000000..f49d8660f902
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/nightly-report/test/nightly-report.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: nightly-report-test
+description: The scheduled report logs the stock counts
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "nightly-report"
+            file: "../nightly-report.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "nightly-report"
+          logMessage: "120 T-shirts and 45 mugs in stock"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/order-generator/test/order-generator.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/order-generator/test/order-generator.citrus.it.yaml
new file mode 100644
index 000000000000..5ffe7828be8a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/order-generator/test/order-generator.citrus.it.yaml
@@ -0,0 +1,24 @@
+name: order-generator-test
+description: The order generator logs a new order with a number and a JSON body
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "order-generator"
+            file: "../order-generator.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "beans.yaml"
+            - "OrderNumber.java"
+  - camel:
+      jbang:
+        verify:
+          integration: "order-generator"
+          logMessage: "New order ORD-1001"
+  - camel:
+      jbang:
+        verify:
+          integration: "order-generator"
+          logMessage: "\"sku\": \"CAMEL-TSHIRT\""
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/properties-and-profiles/test/properties-and-profiles.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/properties-and-profiles/test/properties-and-profiles.citrus.it.yaml
new file mode 100644
index 000000000000..774460494b22
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/properties-and-profiles/test/properties-and-profiles.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: properties-and-profiles-test
+description: The route greets with the values of application.properties
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "properties-and-profiles"
+            file: "../properties-and-profiles.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "properties-and-profiles"
+          logMessage: "Welcome to Camel Shop (development), prices in EUR"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/test/csv-to-json.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/test/csv-to-json.citrus.it.yaml
new file mode 100644
index 000000000000..87a05fcb758f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/test/csv-to-json.citrus.it.yaml
@@ -0,0 +1,21 @@
+name: csv-to-json-test
+description: The csv-to-json example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "csv-to-json"
+            file: "../csv-to-json.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "csv-to-json"
+          logMessage: "Invoice INV-2001"
+  - camel:
+      jbang:
+        verify:
+          integration: "csv-to-json"
+          logMessage: "Invoice INV-2003"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/data-mapping/test/data-mapping.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/data-mapping/test/data-mapping.citrus.it.yaml
new file mode 100644
index 000000000000..7a9eb375cef4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/data-mapping/test/data-mapping.citrus.it.yaml
@@ -0,0 +1,24 @@
+name: data-mapping-test
+description: The data-mapping example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "data-mapping"
+            file: "../data-mapping.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "order.json"
+            - "shipment-mapping.groovy"
+  - camel:
+      jbang:
+        verify:
+          integration: "data-mapping"
+          logMessage: "\"shipmentRef\":\"SHIP-1001\""
+  - camel:
+      jbang:
+        verify:
+          integration: "data-mapping"
+          logMessage: "\"totalPieces\":3"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/groovy/test/groovy.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/groovy/test/groovy.citrus.it.yaml
new file mode 100644
index 000000000000..f4148cb2419c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/groovy/test/groovy.citrus.it.yaml
@@ -0,0 +1,24 @@
+name: groovy-test
+description: The groovy example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "groovy"
+            file: "../groovy.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "order.json"
+            - "order-bad-email.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "groovy"
+          logMessage: "Order ORD-1001 accepted"
+  - camel:
+      jbang:
+        verify:
+          integration: "groovy"
+          logMessage: "Order ORD-1002 rejected"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/json-transform/test/json-transform.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/json-transform/test/json-transform.citrus.it.yaml
new file mode 100644
index 000000000000..906e20673d31
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/json-transform/test/json-transform.citrus.it.yaml
@@ -0,0 +1,23 @@
+name: json-transform-test
+description: The json-transform example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "json-transform"
+            file: "../json-transform.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "order.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "json-transform"
+          logMessage: "\"order\" : \"ORD-1001\""
+  - camel:
+      jbang:
+        verify:
+          integration: "json-transform"
+          logMessage: "\"sku\" : \"CAMEL-TSHIRT\""
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xml-to-json/test/xml-to-json.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xml-to-json/test/xml-to-json.citrus.it.yaml
new file mode 100644
index 000000000000..f46df6a5532b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xml-to-json/test/xml-to-json.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: xml-to-json-test
+description: The xml-to-json example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "xml-to-json"
+            file: "../xml-to-json.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+  - camel:
+      jbang:
+        verify:
+          integration: "xml-to-json"
+          logMessage: "The same order as JSON: {\"id\":\"ORD-1001\""
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xslt/test/xslt.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xslt/test/xslt.citrus.it.yaml
new file mode 100644
index 000000000000..b1078a001031
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xslt/test/xslt.citrus.it.yaml
@@ -0,0 +1,23 @@
+name: xslt-test
+description: The xslt example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "xslt"
+            file: "../xslt.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "packing-slip.xsl"
+  - camel:
+      jbang:
+        verify:
+          integration: "xslt"
+          logMessage: "<packingSlip order=\"ORD-1001\""
+  - camel:
+      jbang:
+        verify:
+          integration: "xslt"
+          logMessage: "<pieces>3</pieces>"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsCitrusTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsCitrusTest.java
new file mode 100644
index 000000000000..2492bc9f7db9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsCitrusTest.java
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A Citrus test, such as test/orders.citrus.it.yaml, is run with camel test 
run and is not a route: the write, edit and
+ * validate tools check its YAML syntax only, with none of the repairs, notes 
and reloads of a route (test-citrus-orders
+ * of the local-model benchmark).
+ */
+class AuthoringToolsCitrusTest {
+
+    private static final String FILE = "test/orders.citrus.it.yaml";
+
+    /** The sample test camel test init writes. */
+    private static final String SAMPLE = """
+            name: orders.citrus.it
+            author: Citrus
+            status: FINAL
+            description: Sample test in YAML
+            variables:
+              - name: message
+                value: Citrus rocks!
+            actions:
+              - echo:
+                  message: "${message}"
+            """;
+
+    /** The test step 1 of test-citrus-orders asks for. */
+    private static final String ORDERS_TEST = """
+            name: orders-test
+            description: The order API accepts the sample order and logs it
+            actions:
+              - camel:
+                  jbang:
+                    run:
+                      integration:
+                        name: "orders"
+                        file: "../orders.camel.yaml"
+                        systemProperties:
+                          file: "../application.properties"
+              - http:
+                  client: "http://localhost:8080";
+                  sendRequest:
+                    POST:
+                      path: "/orders"
+                      contentType: "application/json"
+                      body:
+                        resource:
+                          file: "../sample-order.json"
+              - http:
+                  client: "http://localhost:8080";
+                  receiveResponse:
+                    response:
+                      status: "201"
+              - camel:
+                  jbang:
+                    verify:
+                      integration: "orders"
+                      logMessage: "Order ORD-1001 accepted for C-482 with 2 
line(s)"
+            """;
+
+    private static final String LOG_MESSAGE = "          logMessage: \"Order 
ORD-1001 accepted for C-482 with 2 line(s)\"\n";
+
+    @Test
+    void theNamesCitrusRunsAsYamlTests() {
+        assertThat(SourceValidator.isCitrusTest(FILE)).isTrue();
+        
assertThat(SourceValidator.isCitrusTest("orders.citrus.yaml")).isTrue();
+        
assertThat(SourceValidator.isCitrusTest("orders.citrus.test.yaml")).isTrue();
+        
assertThat(SourceValidator.isCitrusTest("orders.citrus-it.yaml")).isTrue();
+        
assertThat(SourceValidator.isCitrusTest("test/Orders.Citrus.IT.yml")).isTrue();
+        
assertThat(SourceValidator.isCitrusTest("orders.camel.yaml")).isFalse();
+        assertThat(SourceValidator.isCitrusTest("citrus.yaml")).isFalse();
+        
assertThat(SourceValidator.isCitrusTest("orders.citrus.it.xml")).isFalse();
+        
assertThat(SourceValidator.isCitrusTest("test.citrus.d/orders.yaml")).isFalse();
+        
assertThat(SourceValidator.isCitrusTest("test/citrus-application.properties")).isFalse();
+    }
+
+    @Test
+    void aCitrusTestIsWrittenWithoutTheChecksOfARoute(@TempDir Path dir) 
throws IOException {
+        // step 1: written over the sample of camel test init, the test was 
said to have the problem "a Camel YAML file
+        // is a list of entries", and the model made it a list, then a route
+        Files.createDirectories(dir.resolve("test"));
+        Files.writeString(dir.resolve(FILE), SAMPLE);
+
+        JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir, 
FILE, ORDERS_TEST, true);
+
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("overwritten");
+        assertThat(result.get("existingProblems")).isNull();
+        assertThat(result.get("notes")).isNull();
+        
assertThat(result.getString("message")).isEqualTo(AuthoringTools.citrusTestNote(FILE))
+                .contains("camel test run test/orders.citrus.it.yaml");
+        assertThat(Files.readString(dir.resolve(FILE))).isEqualTo(ORDERS_TEST);
+        // the content a route check refuses
+        assertThat(SourceValidator.validateCamelYaml(ORDERS_TEST, new 
DefaultCamelCatalog())).isNotEmpty();
+    }
+
+    @Test
+    void theVariablesOfACitrusTestAreNotSimpleExpressions(@TempDir Path dir) {
+        // the first write of the model was refused: Simple syntax error: 
Unknown function: orderId
+        JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir, 
FILE, """
+                name: orders-test
+                variables:
+                  - name: orderId
+                    value: ORD-1001
+                actions:
+                  - echo:
+                      message: "Order ${orderId} accepted"
+                """, true);
+
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("created");
+    }
+
+    @Test
+    void 
aCitrusTestThatDoesNotParseIsRefusedWithTheLineAndNotRepaired(@TempDir Path 
dir) throws IOException {
+        // a list item two columns off: the write of a route repairs it, the 
write of a test says where it is
+        Files.createDirectories(dir.resolve("test"));
+        Files.writeString(dir.resolve(FILE), SAMPLE);
+
+        JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir, 
FILE, """
+                name: orders-test
+                actions:
+                  - camel:
+                      jbang:
+                        run:
+                          integration:
+                            file: "../orders.camel.yaml"
+                    - http:
+                        client: "http://localhost:8080";
+                """, true);
+
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("invalid");
+        assertThat(result.get("errors").toString()).contains("Line 8: the YAML 
does not parse at column 5")
+                .contains("while parsing a block mapping that starts at line 
3");
+        assertThat(result.get("repaired")).isNull();
+        assertThat(result.get("repairable")).isNull();
+        assertThat(Files.readString(dir.resolve(FILE))).isEqualTo(SAMPLE);
+    }
+
+    @Test
+    void theRunningIntegrationIsNotSaidToReloadACitrusTest(@TempDir Path dir) {
+        // the answer said "The running integration reloaded the file": dev 
mode with --source-dir reloads the routes
+        // when the test changes, but loads no route from it
+        ToolContext ctx = new ToolContext();
+        ctx.selectProcess(ProcessHandle.current().pid());
+
+        JsonObject result = AuthoringTools.writeFile(ctx, dir, FILE, 
ORDERS_TEST, true);
+
+        assertThat(result.getString("status")).isEqualTo("created");
+        assertThat(result.get("reload")).as(String.valueOf(result)).isNull();
+        
assertThat(result.getString("message")).doesNotContain("reload").contains("camel
 test run");
+    }
+
+    @Test
+    void anEditOfACitrusTestIsCheckedForItsYamlOnly(@TempDir Path dir) throws 
IOException {
+        // step 2: a case added after the last action
+        Files.createDirectories(dir.resolve("test"));
+        Files.writeString(dir.resolve(FILE), ORDERS_TEST);
+
+        JsonObject result = AuthoringTools.editFile(new ToolContext(), dir, 
FILE, LOG_MESSAGE, LOG_MESSAGE + """
+                  - http:
+                      client: "http://localhost:8080";
+                      receiveResponse:
+                        response:
+                          status: "400"
+                """);
+
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("edited");
+        assertThat(result.get("reload")).isNull();
+        assertThat(result.getString("message")).contains("camel test run " + 
FILE);
+        assertThat(Files.readString(dir.resolve(FILE))).endsWith("          
status: \"400\"\n");
+
+        JsonObject again = AuthoringTools.editFile(new ToolContext(), dir, 
FILE, LOG_MESSAGE, LOG_MESSAGE);
+        assertThat(again.getString("status")).isEqualTo("unchanged");
+        assertThat(again.getString("message")).contains("camel test 
run").doesNotContain("route does");
+    }
+
+    @Test
+    void validateSourceChecksTheYamlOfACitrusTest() {
+        JsonObject valid = AuthoringTools.validate(new ToolContext(), FILE, 
ORDERS_TEST);
+        
assertThat(valid.getBoolean("valid")).as(String.valueOf(valid)).isTrue();
+        assertThat(valid.getString("message")).startsWith("The YAML is 
valid.").contains("camel test run " + FILE);
+
+        // a quote that is not closed: the parser reads to the end of the 
file, and names the line of the quote
+        JsonObject invalid = AuthoringTools.validate(new ToolContext(), FILE, 
"""
+                name: orders-test
+                actions:
+                  - echo:
+                      message: "Order accepted
+                  - echo:
+                      message: done
+                """);
+        assertThat(invalid.getBoolean("valid")).isFalse();
+        assertThat(invalid.get("errors").toString()).contains("the YAML does 
not parse")
+                .contains("while scanning a quoted scalar that starts at line 
4");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
index 843caf7bab0b..e73ebf10e32b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
@@ -64,6 +64,51 @@ class ExampleToolsRegistryTest {
         assertThat(bundled).as("bundled 
examples").contains("quick-start/timer-log").hasSizeGreaterThan(10);
     }
 
+    @Test
+    void theCitrusTestOfAnExampleIsOneOfItsFiles() throws Exception {
+        // test-citrus-orders of the benchmark: asked to write a Citrus test, 
a model found no sample to read, as the
+        // files of the catalog leave out test/
+        int tests = 0;
+        for (JsonObject entry : rawCatalog()) {
+            if (!entry.getBooleanOrDefault("hasCitrusTests", false)) {
+                continue;
+            }
+            tests++;
+            String name = entry.getString("name");
+            String test = "test/" + name.substring(name.lastIndexOf('/') + 1) 
+ ".citrus.it.yaml";
+
+            JsonObject answer = exampleFile(name, test);
+
+            if (entry.getBooleanOrDefault("bundled", false)) {
+                assertThat(answer.getString("content")).as("test of %s", 
name).contains("actions:")
+                        .isEqualTo(resource("examples/" + name + "/" + test));
+            } else {
+                assertThat(answer.getString("githubUrl")).as("test of %s", 
name).endsWith("/" + name + "/" + test);
+            }
+        }
+        assertThat(tests).isGreaterThan(20);
+    }
+
+    @Test
+    void aSearchForCitrusListsTheExamplesWithATest() throws Exception {
+        long withTest = rawCatalog().stream().filter(e -> 
e.getBooleanOrDefault("hasCitrusTests", false)).count();
+
+        JsonObject citrus = listExamples("citrus");
+
+        assertThat(citrus.getInteger("total")).isEqualTo((int) withTest);
+        for (Object o : citrus.getCollection("examples")) {
+            JsonObject example = (JsonObject) o;
+            String name = example.getString("name");
+            assertThat(example.<List<String>> 
getCollection("files")).as("files of %s", name)
+                    .contains("test/" + name.substring(name.lastIndexOf('/') + 
1) + ".citrus.it.yaml");
+        }
+        // the test of one example
+        JsonObject one = listExamples("csv-to-json test");
+        assertThat(one.getInteger("total")).isEqualTo(1);
+        assertThat(((JsonObject) 
one.getCollection("examples").iterator().next()).getString("name"))
+                .isEqualTo("transform/csv-to-json");
+    }
+
     @Test
     void anUnknownExampleListsTheClosestNames() {
         // the group of 4.22 (beginner), a typo, and a name that is nowhere 
near an example
@@ -118,10 +163,12 @@ class ExampleToolsRegistryTest {
         ToolDescriptor tool = ToolRegistry.findTool("get_example_file");
         assertThat(tool).isNotNull();
         
assertThat(ToolRegistry.EXAMPLE_NAME_DESC).contains("quick-start/timer-log");
-        
assertThat(ToolRegistry.EXAMPLE_FILE_DESC).contains("timer-log.camel.yaml");
+        
assertThat(ToolRegistry.EXAMPLE_FILE_DESC).contains("timer-log.camel.yaml", 
"test/timer-log.citrus.it.yaml");
         JsonObject answer = exampleFile("quick-start/timer-log", 
"timer-log.camel.yaml");
         
assertThat(answer.getString("example")).isEqualTo("quick-start/timer-log");
         assertThat(answer.getString("content")).contains("uri: timer");
+        assertThat(exampleFile("quick-start/timer-log", 
"test/timer-log.citrus.it.yaml").getString("content"))
+                .contains("actions:");
     }
 
     private static JsonObject exampleFile(String example, String file) {
@@ -134,6 +181,11 @@ class ExampleToolsRegistryTest {
         }
     }
 
+    private static JsonObject listExamples(String filter) throws Exception {
+        Object result = ToolRegistry.execute("list_examples", new 
ToolContext(), Map.of("filter", filter));
+        return (JsonObject) Jsoner.deserialize(result.toString());
+    }
+
     private static List<JsonObject> rawCatalog() throws Exception {
         JsonArray array = (JsonArray) 
Jsoner.deserialize(resource("examples/camel-jbang-example-catalog.json"));
         List<JsonObject> catalog = new ArrayList<>();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
index de60251429cc..b423a88ff8f3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
@@ -100,6 +100,35 @@ class ExampleHelperTest {
         assertEquals("connect-service/mqtt", 
filtered.get(0).getString("name"));
     }
 
+    @Test
+    void shouldFilterByCitrusAndByEveryWord() {
+        List<JsonObject> catalog = ExampleHelper.loadCatalog();
+        // the catalog has a flag for the Citrus test of an example, not its 
file: a search for citrus found none
+        List<JsonObject> citrus = ExampleHelper.filterExamples(catalog, 
"citrus");
+        
assertEquals(catalog.stream().filter(ExampleHelper::hasCitrusTests).count(), 
citrus.size());
+        assertEquals(citrus.size(), ExampleHelper.filterExamples(catalog, 
"Citrus tests").size());
+
+        // every word is found, each in any field
+        List<JsonObject> csv = ExampleHelper.filterExamples(catalog, 
"csv-to-json test");
+        assertEquals(1, csv.size());
+        assertEquals("transform/csv-to-json", csv.get(0).getString("name"));
+        assertTrue(ExampleHelper.filterExamples(catalog, "memory-leak 
test").isEmpty(), "the example has no test");
+        assertEquals(catalog.size(), ExampleHelper.filterExamples(catalog, " 
").size());
+    }
+
+    @Test
+    void shouldGetTestFiles() {
+        List<JsonObject> catalog = ExampleHelper.loadCatalog();
+        JsonObject csv = ExampleHelper.findExample(catalog, 
"transform/csv-to-json");
+        assertEquals(List.of("test/csv-to-json.citrus.it.yaml"), 
ExampleHelper.getTestFiles(csv));
+        // not a file to run: camel run --example runs the files of the catalog
+        assertFalse(ExampleHelper.getFiles(csv).stream().anyMatch(f -> 
f.startsWith("test/")));
+
+        JsonObject leak = ExampleHelper.findExample(catalog, 
"showcase/memory-leak");
+        assertFalse(ExampleHelper.hasCitrusTests(leak));
+        assertTrue(ExampleHelper.getTestFiles(leak).isEmpty());
+    }
+
     @Test
     void shouldReturnAllWhenFilterEmpty() {
         List<JsonObject> catalog = ExampleHelper.loadCatalog();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
index 003c76299bb8..da53c3ad410c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
@@ -23,6 +23,7 @@ import java.nio.file.Path;
 import java.util.Properties;
 import java.util.stream.Stream;
 
+import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
 import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.model.Model;
@@ -53,6 +54,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  * Every example is also validated against the canonical YAML schema: the 
examples are what a user or an AI agent starts
  * from, so they must be written in the canonical format and not in the 
deprecated compact notation, which
  * {@code camel run} warns about.
+ *
+ * The Citrus tests of the examples are not routes, so they are left out.
  */
 class ExampleRoutesLoadTest {
 
@@ -75,7 +78,8 @@ class ExampleRoutesLoadTest {
             return files.filter(Files::isRegularFile)
                     .filter(p -> {
                         String name = p.getFileName().toString();
-                        return name.endsWith(".yaml") || name.endsWith(".yml");
+                        // a Citrus test (test/name.citrus.it.yaml) is run by 
camel test run, it is not a route
+                        return (name.endsWith(".yaml") || 
name.endsWith(".yml")) && !SourceValidator.isCitrusTest(name);
                     })
                     .sorted()
                     // use the path relative to the examples root as a stable, 
readable test display name
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
index f39e3fad1e99..3b5ecfcee05c 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
@@ -43,13 +43,14 @@ public class ExampleTools {
                         + "cloud and showcase, in that reading order. Returns 
the groups (level, title, introduction) "
                         + "and the examples in reading order with name, title, 
description, level, order, tags, "
                         + "what they teach (components, EIPs, languages, data 
formats), the infra services they need "
-                        + "(start them with camel infra run), whether they are 
bundled and their files. "
+                        + "(start them with camel infra run), whether they are 
bundled and their files "
+                        + "(test/ has the Citrus test, run with camel test 
run). "
                         + "Call it without arguments for the whole ladder, 
with level for one group, "
-                        + "or with filter to search by name, description or 
tag. "
+                        + "or with filter to search by name, description or 
tag (citrus for the examples with a "
+                        + "Citrus test). "
                         + "Use camel_catalog_example_file to read a file of an 
example.")
     public JsonObject camel_catalog_examples(
-            @ToolArg(description = "Filter examples by name, description, or 
tag (case-insensitive substring match)",
-                     required = false) String filter,
+            @ToolArg(description = ToolRegistry.EXAMPLE_FILTER_DESC, required 
= false) String filter,
             @ToolArg(description = ToolRegistry.EXAMPLE_LEVEL_DESC, required = 
false) String level,
             @ToolArg(description = "Maximum number of examples to return 
(default: 50)",
                      required = false) Integer limit) {
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
index f6f8feeae963..39d642aaace4 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
@@ -77,6 +77,8 @@ public class McpServerInstructions implements 
InitialResponseInfo {
             - which component for a protocol or product: camel_catalog_find
             - a validated YAML sample and where it goes: camel_catalog_sample
             - complete example projects: camel_catalog_examples
+            - a sample Citrus test (test/<name>.citrus.it.yaml, run with camel 
test run): the test/ file of an \
+            example, camel_catalog_examples with filter citrus
             - Maven coordinates (the --dep value) of a Java class: 
camel_dependency_for_class
             - validate YAML/Java/XML/properties before writing: 
camel_validate_source
             - run an integration: camel_run; its log: camel_get_log; its 
errors: camel_get_errors
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
index dc3ab34208c7..d4e6529bbaca 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
@@ -93,6 +93,22 @@ class ExampleToolsTest {
         
assertThat(full.getString("content")).isEqualTo(answer.getString("content"));
     }
 
+    @Test
+    void aSearchForCitrusFindsTheExamplesWithATestToRead() {
+        // test-citrus-orders of the benchmark: a search for citrus found no 
example, and the files of an example left
+        // out its Citrus test
+        JsonObject citrus = new 
ExampleTools().camel_catalog_examples("citrus", null, null);
+
+        List<JsonObject> examples = objects(citrus, "examples");
+        assertThat(examples).hasSizeGreaterThan(20);
+        assertThat(examples).allSatisfy(e -> assertThat(e.<List<String>> 
getCollection("files"))
+                .anyMatch(f -> f.startsWith("test/") && 
f.endsWith(".citrus.it.yaml")));
+
+        JsonObject test = new 
ExampleTools().camel_catalog_example_file("csv-to-json", 
"test/csv-to-json.citrus.it.yaml");
+        
assertThat(test.getString("example")).isEqualTo("transform/csv-to-json");
+        assertThat(test.getString("content")).contains("actions:", 
"../csv-to-json.camel.yaml");
+    }
+
     @Test
     void errorsAreToolErrors() {
         // the messages are tested on the registry tools; here only that they 
reach the client as tool errors
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
index 1ebd193fca08..a458e1e269f8 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
@@ -88,6 +88,13 @@ class McpServerInstructionsTest {
         
assertThat(McpServerInstructions.MAPPED_TOOLS).containsExactlyInAnyOrderElementsOf(named);
     }
 
+    @Test
+    void aSampleCitrusTestIsInTheExamples() {
+        String text = new McpServerInstructions().text();
+
+        assertThat(text).contains("Citrus test", "camel test run", 
"camel_catalog_examples with filter citrus");
+    }
+
     @Test
     void anExplicitPropertyReplacesTheText() {
         McpServerInstructions bean = new McpServerInstructions("4.0.0");

Reply via email to