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 c21135a79196 CAMEL-26495: camel-jbang - camel run and camel_run do not 
start a Citrus test, and say to use camel test run (#28884)
c21135a79196 is described below

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

    CAMEL-26495: camel-jbang - camel run and camel_run do not start a Citrus 
test, and say to use camel test run (#28884)
    
    In the local-model benchmark (test-citrus-orders, step 3) the model ran its
    Citrus test with the camel_run tool (files: test/orders.citrus.it.yaml).
    camel run put the test on the classpath and started an integration with 0
    routes, and the answer was "Started orders-test". The model then read the 
log
    of that empty integration four times, which has no test result.
    
    Claus asked to point to and use camel test ... for all Citrus CLI.
    
    Now:
    - A run of nothing but Citrus tests exits with 2 before anything is printed:
      camel run, camel dev and camel debug of test/orders.citrus.it.yaml, or of 
a
      folder that holds only Citrus tests (camel run test/, or --source-dir):
      "test/orders.citrus.it.yaml is a Citrus test, not a route: run it with
      camel test run test/orders.citrus.it.yaml". Exit code 2 is a wrong command
      line: the file is there, so not 66, and nothing ran, so not 1.
    - Citrus tests given with routes, as by camel run * of a folder with its
      tests next to the routes, are skipped with a line each, and the routes run
      as before: "Skipping orders.citrus.it.yaml: a Citrus test, not a route 
(run
      it with camel test run orders.citrus.it.yaml)". A directory or
      --source-dir with a test folder runs as before.
    - The camel_run tool of the AI tools and of the camel-jbang-mcp server does
      the same. For Citrus tests only it starts nothing, and its error adds "in
      the project directory". With routes it runs the routes, and its answer
      lists the tests in skipped. The description of its files argument says
      that camel test run runs a Citrus test.
    - A Citrus test is named as Citrus names its tests: name.citrus.yaml,
      name.citrus.it.yaml or another name.citrus.*.yaml, the same in .xml or
      .groovy, or a Cucumber .feature. A Java or XML file with a test name, such
      as OrderTest.java or OrderIT.xml, is one when it uses Citrus, as a route
      can have such a name too. A folder holds only Citrus tests when it also
      has no route or other source, no application.properties and no subfolder.
    - The MCP server instructions say to run a Citrus test with camel test run,
      not camel_run. The help of camel run says that camel test run runs a
      Citrus test.
    - The Testing Plugin page created and ran route-test.yaml, which camel test
      run of Citrus 5 skips, as the name does not end like a test name. It now
      shows camel test init orders.citrus.it.yaml and camel test run
      test/orders.citrus.it.yaml. The running page, the MCP server page and the
      4.24 upgrade guide say what camel run does with Citrus tests.
    
    
    Claude-Session: https://claude.ai/code/session_016Y4dDi5iQNgtHrDdz2NpwF
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   6 +
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   7 +-
 .../modules/ROOT/pages/camel-jbang-running.adoc    |   6 +
 .../modules/ROOT/pages/camel-jbang-test.adoc       |  18 ++-
 .../camel/dsl/jbang/core/commands/Debug.java       |   1 +
 .../apache/camel/dsl/jbang/core/commands/Run.java  |  83 +++++++++++++-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |   3 +-
 .../core/commands/ai/IntegrationLauncher.java      |  62 ++++++++++-
 .../dsl/jbang/core/commands/CliHelpGoldenTest.java |   4 +-
 .../core/commands/CommandInputErrorsTest.java      |  25 +++++
 .../dsl/jbang/core/commands/RunDirectoryTest.java  |  16 +++
 .../camel/dsl/jbang/core/commands/RunTest.java     |  21 ++++
 .../core/commands/ai/IntegrationLauncherTest.java  |  70 ++++++++++++
 .../golden/citrus/test/orders.citrus.it.yaml       |  22 ++++
 .../src/test/resources/golden/cli-help.txt         |  19 +++-
 .../jbang/core/commands/mcp/AuthoringTools.java    |   2 +-
 .../core/commands/mcp/McpServerInstructions.java   |   2 +
 .../core/commands/mcp/AuthoringToolsTest.java      |  24 ++++
 .../commands/mcp/McpServerInstructionsTest.java    |   9 ++
 .../org/apache/camel/dsl/jbang/run/RunFiles.java   | 114 ++++++++++++++++++-
 .../apache/camel/dsl/jbang/run/RunFilesTest.java   | 122 +++++++++++++++++++++
 21 files changed, 615 insertions(+), 21 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 1bc188419d61..0d5fe236d6a8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1301,6 +1301,12 @@ or follows, like Enter does.
 * A file that does not exist exits with `66` and `File does not exist: <file>` 
for every kind of file, before anything is
   printed. Files together with `--source-dir`, and `--background` with a 
runtime that does not support it, exit with
   `2`.
+* Only a run of nothing but Citrus tests exits with `2` and says to run them 
with `camel test run`, before anything is
+  printed: `camel run test/orders.citrus.it.yaml`, or `camel run test/` of a 
folder that holds only Citrus tests.
+  Before, it ran an integration with no routes. Citrus tests given with 
routes, such as by `camel run *`, are skipped
+  with a line each (`Skipping <file>: a Citrus test, not a route (run it with 
camel test run <file>)`), and the routes
+  run. A directory or `--source-dir` with a `test` folder runs as before. The 
`camel_run` tool of the AI tools does the
+  same: it starts nothing for Citrus tests only, and its answer lists the 
tests it skipped.
 
 === camel-jbang - camel explain and camel harden
 
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 8278cf1b8668..d3a20847e436 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -109,7 +109,8 @@ questions that no tool answers and that agents otherwise go 
hunting for:
   source that passes the schema can still fail at runtime, so run it and read 
the log.
 * Which tool answers which question, by tool name: `camel_catalog_doc`, 
`camel_catalog_find`,
   `camel_catalog_sample`, `camel_catalog_examples`, 
`camel_dependency_for_class`, `camel_validate_source`,
-  `camel_run`, `camel_get_log`, `camel_get_errors` and 
`camel_project_overview`.
+  `camel_run`, `camel_get_log`, `camel_get_errors` and 
`camel_project_overview`; and that a Citrus test runs with
+  `camel test run` in the project directory, not with `camel_run`.
 
 The first line is self-contained, as some hosts show only that line.
 
@@ -450,6 +451,10 @@ passed as the file name, is answered with the argument the 
source goes in.
   lines (also a message over several lines, such as the diagnostics of a Java 
class that does not compile) and
   the first line of the user's code, without the stack frames. A failed 
`camel_control restart` answers the
   same way.
+  A Citrus test, such as `test/orders.citrus.it.yaml`, is not a route. With 
nothing but Citrus tests to run (the
+  files, or a directory that holds only Citrus tests) nothing is started, and 
the error says to run them with
+  `camel test run` in the project directory. Citrus tests given with routes 
are skipped: the routes start, and
+  `skipped` in the answer lists the tests.
 
 | `camel_control`
 | Controls a running integration: `stop`, `kill`, `restart` (picks up edited 
files without dev mode),
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
index a5db9aab6bb2..bf510a8f263c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
@@ -69,6 +69,12 @@ Without `--source-dir`, Camel only watches the specific 
files you listed on the
 
 NOTE: You cannot combine files and source dir: `camel run abc.java 
--source-dir=mycode` is not allowed.
 
+A Citrus test, such as `test/orders.citrus.it.yaml`, is not a route. A run of 
nothing but Citrus tests,
+`camel run test/orders.citrus.it.yaml` or `camel run test/` of a folder that 
holds only Citrus tests, exits with
+code `2` and says to run them with `camel test run` (see 
xref:camel-jbang-test.adoc[Testing Plugin]).
+Citrus tests given with routes, such as by `camel run *`, are skipped with a 
line each, and the routes run.
+A directory or `--source-dir` with a `test` folder runs as before.
+
 === Data folders in the source directory
 
 Routes often read and write files in folders of their own, such as an `inbox` 
they consume and an `archive` they
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-test.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-test.adoc
index 273364e4e1d5..b6f30320ecd6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-test.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-test.adoc
@@ -13,26 +13,32 @@ TIP: Run `camel test --help` to see all available 
subcommands.
 
 == Initialize tests
 
-Create an empty test file as a starting point:
+Create a test file as a starting point:
 
 [source,bash]
 ----
-camel test init route-test.yaml
+camel test init orders.citrus.it.yaml
 ----
 
-The file extension determines the test language: YAML, Java, XML, or Cucumber 
(`.feature`).
+The test is created as `test/orders.citrus.it.yaml`: tests go in a `test` 
folder, apart from the routes.
 
-NOTE: Tests are created in a `test` subfolder to separate them from runtime 
sources. You don't need to navigate there — the plugin handles the working 
directory automatically.
+The end of the file name says that the file is a test, and in which language:
+`orders.citrus.it.yaml` (YAML), `orders.citrus.it.xml` (XML), `OrdersIT.java` 
(Java) or `orders.feature` (Cucumber).
+`camel test run` only runs a file whose name ends like that.
 
 == Run tests
 
-Run tests locally with JUnit Jupiter — no project setup needed:
+Run a test from the project folder. It runs with JUnit Jupiter, and needs no 
project setup:
 
 [source,bash]
 ----
-camel test run route-test.yaml
+camel test run test/orders.citrus.it.yaml
 ----
 
+`camel run` does not run a test: `camel run test/orders.citrus.it.yaml` exits 
with code `2` and says to use
+`camel test run`, and a test given with routes is skipped. A test usually 
starts the integration itself, see
+<<_running_the_camel_integration,Running the Camel integration>>.
+
 == Project export
 
 When you export a project, the test plugin automatically includes test 
resources as test-scoped Maven dependencies in `src/test/java` and 
`src/test/resources`:
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
index d84f6a3d4447..6f788122dea0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
@@ -173,6 +173,7 @@ public class Debug extends Run {
         requireInteractiveTerminal("camel debug");
         if (!remoteAttach) {
             checkFilesExist();
+            skipCitrusTests();
         }
         printConfigurationValues("Debugging integration with the following 
configuration:");
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index 1762a42435d9..bdbbee658209 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -50,6 +50,7 @@ import 
org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
 import org.apache.camel.dsl.jbang.run.ConfiguredRun;
 import org.apache.camel.dsl.jbang.run.ProjectRunner;
 import org.apache.camel.dsl.jbang.run.RunContext;
+import org.apache.camel.dsl.jbang.run.RunFiles;
 import org.apache.camel.dsl.jbang.run.RunPipeline;
 import org.apache.camel.dsl.jbang.run.RunProfileProperties;
 import org.apache.camel.dsl.jbang.run.RunRequest;
@@ -116,7 +117,8 @@ public class Run extends CamelCommand {
     private Printer quietPrinter;
     private boolean mcpStdioMode;
 
-    @Parameters(description = "The Camel file(s) to run. If no files specified 
then application.properties is used as source for which files to run.",
+    @Parameters(description = "The Camel file(s) to run. If no files specified 
then application.properties is used as source for which files to run."
+                              + " A Citrus test is run with camel test run.",
                 arity = "0..9", paramLabel = "<files>", parameterConsumer = 
FilesConsumer.class)
     Path[] filePaths; // Defined only for file path completion; the field 
never used
 
@@ -346,6 +348,7 @@ public class Run extends CamelCommand {
         }
 
         checkFilesExist();
+        skipCitrusTests();
         printConfigurationValues("Running integration with the following 
configuration:");
         // run
         return run();
@@ -360,10 +363,8 @@ public class Run extends CamelCommand {
             return;
         }
         for (String file : files) {
-            String path = file.startsWith("file:") ? file.substring(5) : file;
-            String scheme = getScheme(path);
-            if (scheme != null && scheme.length() > 1 || path.contains("*") || 
path.contains("?")) {
-                // a single letter is a drive on Windows
+            String path = localPath(file);
+            if (path == null) {
                 continue;
             }
             boolean exists;
@@ -378,6 +379,78 @@ public class Run extends CamelCommand {
         }
     }
 
+    /**
+     * A Citrus test, such as test/orders.citrus.it.yaml, is not a route: 
camel test run runs it. A run of nothing but
+     * Citrus tests, also of a folder that holds only Citrus tests (camel run 
test/), fails with exit code 2 (a wrong
+     * command line) before anything is prepared or printed, as it would start 
an integration with no routes. Citrus
+     * tests given with routes, such as by camel run *, are left out with a 
line each, and the routes run. A folder or
+     * --source-dir with a test folder runs as before.
+     */
+    void skipCitrusTests() {
+        Path folder = folderToRun();
+        if (folder != null) {
+            List<String> tests = 
RunFiles.citrusTestsOnly(folder).stream().map(p -> 
p.normalize().toString()).toList();
+            if (!tests.isEmpty()) {
+                throw CliException.usage(RunFiles.citrusTestMessage(tests));
+            }
+            return;
+        }
+        if (files == null) {
+            return;
+        }
+        List<String> given = new ArrayList<>();
+        List<String> tests = new ArrayList<>();
+        for (String file : files) {
+            String path = localPath(file);
+            try {
+                if (path != null && RunFiles.isCitrusTest(Paths.get(path))) {
+                    given.add(file);
+                    tests.add(path);
+                }
+            } catch (InvalidPathException e) {
+                // not a file name of this file system, which checkFilesExist 
reports
+            }
+        }
+        if (!given.isEmpty() && given.size() == files.size()) {
+            throw CliException.usage(RunFiles.citrusTestMessage(tests));
+        }
+        for (String test : tests) {
+            printer().printDiagnostic(RunFiles.citrusTestSkipMessage(test));
+        }
+        files.removeAll(given);
+    }
+
+    /**
+     * The folder to run: the --source-dir, or a folder given as the only file 
to run; null otherwise.
+     */
+    private Path folderToRun() {
+        String dir = null;
+        if (sourceDir != null && (files == null || files.isEmpty())) {
+            dir = sourceDir;
+        } else if (sourceDir == null && files != null && files.size() == 1) {
+            dir = localPath(files.get(0));
+        }
+        try {
+            return dir != null && Files.isDirectory(Paths.get(dir)) ? 
Paths.get(dir) : null;
+        } catch (InvalidPathException e) {
+            return null;
+        }
+    }
+
+    /**
+     * The path of a file to run on the file system, without file:, or null 
for a remote file (such as github: and
+     * https:) or a wildcard.
+     */
+    private static String localPath(String file) {
+        String path = file.startsWith("file:") ? file.substring(5) : file;
+        String scheme = getScheme(path);
+        if (scheme != null && scheme.length() > 1 || path.contains("*") || 
path.contains("?")) {
+            // a single letter is a drive on Windows
+            return null;
+        }
+        return path;
+    }
+
     private int listExamples(String group) {
         List<JsonObject> catalog = ExampleHelper.loadCatalog();
         if (catalog.isEmpty()) {
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 d62d000be043..096fc24197be 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
@@ -296,7 +296,8 @@ public final class AuthoringTools {
         registry.accept(tool("camel_run",
                 "Starts an integration with camel run in a separate process, 
in dev mode by default (a changed or added file is reloaded). Returns the pid 
and log file; camel_get_log and camel_control follow it.")
                 .param("directory", "string", "Project directory to run in", 
true)
-                .param("files", "string", "Source files to run, 
comma-separated (default: the whole directory)",
+                .param("files", "string", "Source files to run, 
comma-separated (default: the whole directory); not a"
+                                          + " Citrus test, which camel test 
run runs",
                         false)
                 .param("name", "string", "Integration name (default: from the 
first file)", false)
                 .param("dev", "boolean", "Dev mode with reload on file change 
(default true)", false)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index a688b8839953..d2f70038d14d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
@@ -34,6 +35,7 @@ import org.apache.camel.dsl.jbang.core.common.BackgroundLog;
 import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
 import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.dsl.jbang.run.RunFiles;
 import org.apache.camel.dsl.jbang.run.common.LogFileReader;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -100,7 +102,9 @@ public final class IntegrationLauncher {
     }
 
     /**
-     * Starts {@code camel run} detached in the given directory.
+     * Starts {@code camel run} detached in the given directory. A Citrus test 
is not run: with nothing but Citrus tests
+     * to run (the files, or a directory of only Citrus tests) nothing is 
started, and the answer says to run them with
+     * {@code camel test run}; Citrus tests given with routes are left out, 
and the answer lists them in skipped.
      *
      * @param  directory the project directory (the working directory of the 
process)
      * @param  files     the source files to run, relative to the directory; 
empty runs every route file in it
@@ -108,9 +112,40 @@ public final class IntegrationLauncher {
      *                   restart, else the default
      * @param  dev       dev mode (reload on file changes)
      * @param  extraArgs further {@code camel run} arguments
-     * @return           status started (with pid, name, log), failed (with 
the output) or starting
+     * @return           status started (with pid, name, log), failed (with 
the output or the error) or starting
      */
     public static JsonObject run(Path directory, List<String> files, String 
name, boolean dev, List<String> extraArgs) {
+        boolean sourceDir = files == null || files.isEmpty();
+        List<String> tests = sourceDir
+                ? RunFiles.citrusTestsOnly(directory).stream().map(p -> 
p.getFileName().toString()).toList()
+                : citrusTests(directory, files);
+        if (!tests.isEmpty() && (sourceDir || tests.size() == files.size())) {
+            // a model ran its Citrus test with camel_run, and camel run 
started an integration with no routes
+            // (test-citrus-orders of the benchmark)
+            JsonObject result = new JsonObject();
+            result.put("directory", directory.toString());
+            result.put("status", "failed");
+            result.put("error", RunFiles.citrusTestMessage(tests) + " in the 
project directory");
+            return result;
+        }
+        if (tests.isEmpty()) {
+            return launch(directory, files, name, dev, extraArgs);
+        }
+        List<String> routes = new ArrayList<>(files);
+        routes.removeAll(tests);
+        JsonObject result = launch(directory, routes, name, dev, extraArgs);
+        result.put("skipped", new JsonArray(tests));
+        String message = result.getString("message");
+        result.put("message", (message != null ? message + " " : "") + 
"Skipped: " + RunFiles.citrusTestMessage(tests)
+                              + " in the project directory.");
+        return result;
+    }
+
+    /**
+     * Starts {@code camel run} detached in the given directory, see {@link 
#run(Path, List, String, boolean, List)}.
+     */
+    private static JsonObject launch(
+            Path directory, List<String> files, String name, boolean dev, 
List<String> extraArgs) {
         String runName = runName(directory, name);
         boolean before = runName != null && !runName.equals(name);
         List<String> cmd = new ArrayList<>(camelCommand.get());
@@ -217,6 +252,29 @@ public final class IntegrationLauncher {
         return result;
     }
 
+    /**
+     * The Citrus tests among the files to run, such as 
test/orders.citrus.it.yaml: camel test run runs them.
+     *
+     * @param  directory the project directory
+     * @param  files     the files to run, relative to the directory; null or 
empty runs the directory
+     * @return           the Citrus tests, as given
+     */
+    static List<String> citrusTests(Path directory, List<String> files) {
+        List<String> answer = new ArrayList<>();
+        if (files != null) {
+            for (String file : files) {
+                try {
+                    if (RunFiles.isCitrusTest(directory.resolve(file))) {
+                        answer.add(file);
+                    }
+                } catch (InvalidPathException e) {
+                    // not a file name, which camel run reports
+                }
+            }
+        }
+        return answer;
+    }
+
     /**
      * File extensions {@code camel run} loads from a project directory, as 
{@code camel run *} would pass them: the
      * three DSLs (see {@code SourceHelper.ACCEPTED_FILE_EXT}) and the 
properties files.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliHelpGoldenTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliHelpGoldenTest.java
index 8efc5bd39616..fb44fbe261b9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliHelpGoldenTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CliHelpGoldenTest.java
@@ -72,7 +72,9 @@ class CliHelpGoldenTest extends CamelCommandBaseTestSupport {
             List.of("ps", "--no-such-option", "--json"),
             List.of("config", "get", "no-such-key"),
             List.of("stop", "no-such-integration"),
-            List.of("run", "no-such-file.yaml"));
+            List.of("run", "no-such-file.yaml"),
+            List.of("run", 
"src/test/resources/golden/citrus/test/orders.citrus.it.yaml"),
+            List.of("run", "src/test/resources/golden/citrus/test"));
 
     private final Map<String, String> camelProperties = new TreeMap<>();
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
index ad9680c25e1e..70555ada9c04 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CommandInputErrorsTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.List;
 
 import org.apache.camel.dsl.jbang.core.commands.action.EvalExpressionCommand;
 import org.apache.camel.dsl.jbang.core.commands.action.TransformMessageAction;
@@ -164,6 +165,30 @@ class CommandInputErrorsTest {
         assertThat(printer.getOutput()).isEmpty();
     }
 
+    @Test
+    void runNothingButCitrusTests() throws Exception {
+        Path test = 
Files.createDirectories(dir.resolve("test")).resolve("orders.citrus.it.yaml");
+        Files.writeString(test, "name: orders-test\nactions:\n  - echo:\n      
message: hi\n");
+        String message = test + " is a Citrus test, not a route: run it with 
camel test run " + test;
+
+        // a Citrus test is not a route: camel test run runs it, and nothing 
is prepared or printed
+        for (String file : List.of(test.toString(), "file:" + test)) {
+            assertThatThrownBy(() -> call(new Run(main), file))
+                    .isInstanceOf(CliException.class)
+                    .hasMessage(message)
+                    .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+        }
+        // the same for a folder that holds only Citrus tests, also with camel 
dev
+        String folder = test.getParent().toString();
+        assertThatThrownBy(() -> call(new Run(main), folder))
+                .isInstanceOf(CliException.class).hasMessage(message)
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+        assertThatThrownBy(() -> call(new Dev(main), "--source-dir=" + folder))
+                .isInstanceOf(CliException.class).hasMessage(message)
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+        assertThat(printer.getOutput()).isEmpty();
+    }
+
     @Test
     void runFilesAndSourceDir() throws Exception {
         Path file = Files.writeString(dir.resolve("orders.yaml"), "- from:\n   
 uri: timer:x\n");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunDirectoryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunDirectoryTest.java
index c20eae43df40..d37d7b829873 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunDirectoryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunDirectoryTest.java
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
 
 /**
  * camel run . (or camel run dirName) runs the directory as --source-dir, so 
dev mode reloads changed and new files.
@@ -46,6 +47,21 @@ class RunDirectoryTest {
         assertThat(run(dir.toString(), 
"--runtime=main").runsDirectoryAsSourceDir(dir)).isTrue();
     }
 
+    @Test
+    void directoryWithACitrusTestFolderRunsAsBefore(@TempDir Path dir) throws 
Exception {
+        Files.writeString(dir.resolve("orders.camel.yaml"), "- route:\n");
+        Files.createDirectories(dir.resolve("test"));
+        Files.writeString(dir.resolve("test/orders.citrus.it.yaml"), "name: 
orders-test\n");
+
+        // only a run of nothing but Citrus tests is refused (camel test run 
runs them), not a project with a test folder
+        for (Run run : List.of(run(dir.toString()), run("--source-dir=" + dir),
+                run(dir.resolve("orders.camel.yaml").toString()))) {
+            assertThatCode(run::checkFilesExist).doesNotThrowAnyException();
+            assertThatCode(run::skipCitrusTests).doesNotThrowAnyException();
+        }
+        assertThat(run(dir.toString()).runsDirectoryAsSourceDir(dir)).isTrue();
+    }
+
     @Test
     void mavenProjectKeepsRunningItsFiles(@TempDir Path dir) throws Exception {
         Files.writeString(dir.resolve("pom.xml"), "<project/>");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
index 0ca5a3c8faab..6ee83315cc18 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.camel.dsl.jbang.core.commands;
 
+import java.nio.file.Files;
 import java.nio.file.Path;
 
 import org.apache.camel.FailedToStartRouteException;
@@ -172,6 +173,26 @@ class RunTest extends CamelCommandBaseTestSupport {
         Assertions.assertNotNull(command.example);
     }
 
+    @Test
+    void citrusTestsGivenWithRoutesAreSkipped(@TempDir Path dir) throws 
Exception {
+        Path route = Files.writeString(dir.resolve("orders.camel.yaml"), "- 
from:\n    uri: timer:tick\n");
+        Path test = Files.writeString(dir.resolve("orders.citrus.it.yaml"), 
"name: orders-test\nactions: []\n");
+        Path feature = Files.writeString(dir.resolve("orders.feature"), 
"Feature: orders\n");
+
+        // camel run * of a folder with its Citrus tests next to the routes: 
the routes run as before
+        Run command = new Run(new CamelJBangMain().withPrinter(printer));
+        CommandLineTestHelper.parse(command, route.toString(), 
test.toString(), feature.toString());
+        command.skipCitrusTests();
+
+        assertThat(command.files).containsExactly(route.toString());
+        assertThat(printer.getStderr()).isEqualTo(
+                "Skipping " + test + ": a Citrus test, not a route (run it 
with camel test run " + test + ")\n"
+                                                  + "Skipping " + feature
+                                                  + ": a Citrus test, not a 
route (run it with camel test run " + feature
+                                                  + ")");
+        assertThat(printer.getStdout()).isEmpty();
+    }
+
     @Test
     void jfrIsDisabledByDefault() throws Exception {
         Run command = new Run(new CamelJBangMain());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
index 023b96b7b55f..cf83067aa518 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
@@ -20,7 +20,11 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.function.Supplier;
 
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -65,6 +69,72 @@ class IntegrationLauncherTest {
                         "--port=9000");
     }
 
+    /**
+     * A model ran its Citrus test with camel_run, and camel run started an 
integration with no routes
+     * (test-citrus-orders of the benchmark): nothing is started, and the 
answer says to run it with camel test run.
+     */
+    @Test
+    void aCitrusTestIsNotStarted(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("orders.camel.yaml"), "- from:\n    uri: 
timer:tick\n    steps: []\n");
+        Path test = Files.createDirectories(dir.resolve("test"));
+        Files.writeString(test.resolve("orders.citrus.it.yaml"), "name: 
orders-test\nactions: []\n");
+        Supplier<List<String>> previous = IntegrationLauncher.camelCommand;
+        IntegrationLauncher.camelCommand = () -> {
+            throw new AssertionError("camel run is started");
+        };
+        try {
+            JsonObject result = IntegrationLauncher.run(dir, 
List.of("test/orders.citrus.it.yaml"), "orders-test", true,
+                    List.of());
+            assertThat(result.getString("status")).isEqualTo("failed");
+            
assertThat(result.getString("error")).isEqualTo("test/orders.citrus.it.yaml is 
a Citrus test, not a route:"
+                                                            + " run it with 
camel test run test/orders.citrus.it.yaml in"
+                                                            + " the project 
directory");
+            assertThat(result.containsKey("command")).isFalse();
+            assertThat(result.containsKey("pid")).isFalse();
+
+            // the test folder as the directory to run (camel run --source-dir)
+            result = IntegrationLauncher.run(test, List.of(), null, true, 
List.of());
+            assertThat(result.getString("status")).isEqualTo("failed");
+            
assertThat(result.getString("error")).isEqualTo("orders.citrus.it.yaml is a 
Citrus test, not a route: run it"
+                                                            + " with camel 
test run orders.citrus.it.yaml in the project"
+                                                            + " directory");
+        } finally {
+            IntegrationLauncher.camelCommand = previous;
+        }
+        // the route itself is no Citrus test
+        assertThat(IntegrationLauncher.citrusTests(dir, 
List.of("orders.camel.yaml"))).isEmpty();
+        assertThat(IntegrationLauncher.citrusTests(dir, null)).isEmpty();
+    }
+
+    /** A model may list every file of the project: the routes run, and the 
answer lists the test it skipped. */
+    @Test
+    void aCitrusTestGivenWithTheRoutesIsSkipped(@TempDir Path dir) throws 
Exception {
+        Files.writeString(dir.resolve("orders.camel.yaml"), "- from:\n    uri: 
timer:tick\n    steps: []\n");
+        Files.createDirectories(dir.resolve("test"));
+        Files.writeString(dir.resolve("test/orders.citrus.it.yaml"), "name: 
orders-test\nactions: []\n");
+        // a program that ends at once, in place of camel run, and a home of 
its own for its console output
+        Path exit = Files.writeString(dir.resolve("Exit.java"), "class Exit { 
public static void main(String[] a) { } }");
+        String java = Path.of(System.getProperty("java.home"), "bin", 
"java").toString();
+        Path previousHome = CommandLineHelper.getHomeDir();
+        Supplier<List<String>> previous = IntegrationLauncher.camelCommand;
+        
CommandLineHelper.useHomeDir(Files.createDirectories(dir.resolve("home")).toString());
+        IntegrationLauncher.camelCommand = () -> List.of(java, 
exit.toString());
+        try {
+            JsonObject result = IntegrationLauncher.run(dir, 
List.of("orders.camel.yaml", "test/orders.citrus.it.yaml"),
+                    "orders", true, List.of());
+
+            assertThat(result.getString("command")).contains(" run 
orders.camel.yaml --dev")
+                    .doesNotContain("orders.citrus.it.yaml");
+            assertThat((JsonArray) 
result.get("skipped")).containsExactly("test/orders.citrus.it.yaml");
+            assertThat(result.getString("message")).endsWith("Skipped: 
test/orders.citrus.it.yaml is a Citrus test, not a"
+                                                             + " route: run it 
with camel test run"
+                                                             + " 
test/orders.citrus.it.yaml in the project directory.");
+        } finally {
+            IntegrationLauncher.camelCommand = previous;
+            CommandLineHelper.useHomeDir(previousHome.toString());
+        }
+    }
+
     /**
      * CAMEL-25364: a failed start returns what went wrong, not the last stack 
frames of the runtime. The cause and the
      * first frame of the user's code are kept, the frames are counted.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/citrus/test/orders.citrus.it.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/citrus/test/orders.citrus.it.yaml
new file mode 100644
index 000000000000..06a9d46afa6b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/citrus/test/orders.citrus.it.yaml
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+name: orders-test
+description: A Citrus test, which camel test run runs and camel run does not 
(CliHelpGoldenTest)
+actions:
+  - echo:
+      message: "Citrus rocks!"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index 7161ada1b54f..b9bd0c10e099 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -1664,7 +1664,8 @@ Usage: camel debug [-h] [--ago] [--background] 
[--backlog-trace] [--chaos]
 Debug local Camel integration
       [<files>...]           The Camel file(s) to run. If no files specified
                                then application.properties is used as source
-                               for which files to run.
+                               for which files to run. A Citrus test is run
+                               with camel test run.
   -h, --help                 Display the help and sub-commands
       --runtime=<runtime>    Runtime (jbang, camel-main, spring-boot, quarkus).
                                The jbang runtime runs in-process in the Camel
@@ -2646,7 +2647,8 @@ Usage: camel dev [-h] [--background] [--backlog-trace] 
[--chaos] [--console]
 Run in dev mode with live reload
       [<files>...]           The Camel file(s) to run. If no files specified
                                then application.properties is used as source
-                               for which files to run.
+                               for which files to run. A Citrus test is run
+                               with camel test run.
   -h, --help                 Display the help and sub-commands
       --runtime=<runtime>    Runtime (jbang, camel-main, spring-boot, quarkus).
                                The jbang runtime runs in-process in the Camel
@@ -4746,7 +4748,8 @@ Usage: camel run [-h] [--background] [--backlog-trace] 
[--chaos] [--console]
 Run as local Camel integration
       [<files>...]           The Camel file(s) to run. If no files specified
                                then application.properties is used as source
-                               for which files to run.
+                               for which files to run. A Citrus test is run
+                               with camel test run.
   -h, --help                 Display the help and sub-commands
       --runtime=<runtime>    Runtime (jbang, camel-main, spring-boot, quarkus).
                                The jbang runtime runs in-process in the Camel
@@ -6185,3 +6188,13 @@ stderr:
 exit: 66
 stderr:
   ERROR: File does not exist: no-such-file.yaml
+
+== camel run src/test/resources/golden/citrus/test/orders.citrus.it.yaml
+exit: 2
+stderr:
+  ERROR: src/test/resources/golden/citrus/test/orders.citrus.it.yaml is a 
Citrus test, not a route: run it with camel test run 
src/test/resources/golden/citrus/test/orders.citrus.it.yaml
+
+== camel run src/test/resources/golden/citrus/test
+exit: 2
+stderr:
+  ERROR: src/test/resources/golden/citrus/test/orders.citrus.it.yaml is a 
Citrus test, not a route: run it with camel test run 
src/test/resources/golden/citrus/test/orders.citrus.it.yaml
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index d97bfc9affb1..7874563574de 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -342,7 +342,7 @@ public class AuthoringTools {
     public JsonObject camel_run(
             @ToolArg(description = "Project directory to run in (absolute 
path)", required = true) String directory,
             @ToolArg(description = "Source files to run, comma-separated 
(default: every route file in the"
-                                   + " directory)",
+                                   + " directory); not a Citrus test, which 
camel test run runs",
                      required = false) String files,
             @ToolArg(description = "Integration name (default: from the first 
file)", required = false) String name,
             @ToolArg(description = "Dev mode with reload on file change 
(default true)", required = false) Boolean dev) {
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 39d642aaace4..763d114a306f 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
@@ -83,6 +83,8 @@ public class McpServerInstructions implements 
InitialResponseInfo {
             - 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
             - routes, endpoints and mistakes of an existing project: 
camel_project_overview
+            - run a Citrus test (test/<name>.citrus.it.yaml): camel test run 
<file> in the project directory, not \
+            camel_run
             """;
 
     @ConfigProperty(name = "quarkus.mcp.server.server-info.instructions")
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index 31a2216ed4b2..2ee5d19ac70b 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -37,6 +37,7 @@ import 
org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -129,6 +130,29 @@ class AuthoringToolsTest {
                 .contains("message: hello").contains("message: goodbye");
     }
 
+    /**
+     * A model ran its Citrus test with camel_run, and camel run started an 
integration with no routes
+     * (test-citrus-orders of the benchmark): nothing is started, and the 
error says to run it with camel test run.
+     */
+    @Test
+    void camelRunOfACitrusTestSaysToRunItWithCamelTestRun(@TempDir Path dir) 
throws Exception {
+        Files.createDirectories(dir.resolve("test"));
+        Files.writeString(dir.resolve("test/orders.citrus.it.yaml"), "name: 
orders-test\nactions: []\n");
+
+        assertThatThrownBy(() -> tools.camel_run(dir.toString(), 
"test/orders.citrus.it.yaml", "orders-test", null))
+                .isInstanceOf(ToolCallException.class)
+                .satisfies(e -> {
+                    JsonObject answer = Jsoner.deserialize(e.getMessage(), new 
JsonObject());
+                    assertThat(answer.getString("status")).isEqualTo("failed");
+                    
assertThat(answer.getString("error")).isEqualTo("test/orders.citrus.it.yaml is 
a Citrus test, not a"
+                                                                    + " route: 
run it with camel test run"
+                                                                    + " 
test/orders.citrus.it.yaml in the project"
+                                                                    + " 
directory");
+                    // no camel run command was started
+                    assertThat(answer.containsKey("command")).isFalse();
+                });
+    }
+
     @Test
     void anUnknownIntegrationNameIsAnError(@TempDir Path home) {
         // a home of its own: the log tool looks for the previous run of an 
integration that is not running
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 a458e1e269f8..c4bb6d78031e 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
@@ -72,6 +72,15 @@ class McpServerInstructionsTest {
                 "Which tool:");
     }
 
+    @Test
+    void aCitrusTestRunsWithCamelTestRunNotCamelRun() {
+        String text = new McpServerInstructions().text();
+
+        // a model ran its Citrus test with camel_run, which started an 
integration with no routes
+        assertThat(text).contains("- run a Citrus test 
(test/<name>.citrus.it.yaml): camel test run <file> in the project"
+                                  + " directory, not camel_run");
+    }
+
     @Test
     void everyMappedToolIsMentionedAndIsAToolOfThisServer() throws IOException 
{
         String text = new McpServerInstructions().text();
diff --git 
a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/RunFiles.java
 
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/RunFiles.java
index 37a1c6a94579..ee04c0df20fb 100644
--- 
a/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/RunFiles.java
+++ 
b/dsl/camel-jbang/camel-jbang-run/src/main/java/org/apache/camel/dsl/jbang/run/RunFiles.java
@@ -17,14 +17,18 @@
 package org.apache.camel.dsl.jbang.run;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.run.common.Source;
 import org.apache.camel.dsl.jbang.run.common.SourceHelper;
@@ -36,7 +40,8 @@ import org.apache.camel.xml.io.util.XmlStreamInfo;
 
 /**
  * What kind of file a file to run is: a route (or another Camel DSL file), a 
properties file, a script, a Groovy
- * source, a TLS file, a JKube file, a readme, or a file for the classpath; 
and which files are skipped.
+ * source, a TLS file, a JKube file, a readme, or a file for the classpath; 
which files are skipped; and a Citrus test,
+ * which is not run, as is a folder of only Citrus tests.
  *
  * @since 4.24
  */
@@ -66,6 +71,16 @@ public final class RunFiles {
     private static final Set<String> ACCEPTED_YAML_ROOT_ELEMENTS
             = new HashSet<>(Arrays.asList(ACCEPTED_YAML_ROOT_ELEMENT_NAMES));
 
+    /**
+     * The names of a Citrus test that no route has: name.citrus.yaml, 
name.citrus.it.yaml and any other
+     * name.citrus.*.yaml, the same in .xml and .groovy, and a Cucumber 
feature.
+     */
+    private static final Pattern CITRUS_TEST = 
Pattern.compile(".+\\.citrus(?:[.-].+)?\\.(?:ya?ml|xml|groovy)|.+\\.feature");
+
+    /** The names Citrus runs as a test that a route can have as well: 
NameTest.java, NameIT.xml... */
+    private static final Pattern CITRUS_TEST_CLASS
+            = 
Pattern.compile(".*(?:Test|IT|TestCase|ITCase)\\.java|.*(?:Test|IT)\\.xml");
+
     private RunFiles() {
     }
 
@@ -216,4 +231,101 @@ public final class RunFiles {
         String on = FileUtil.onlyName(FileUtil.stripPath(name), true);
         return on.toLowerCase(Locale.ROOT).startsWith("readme");
     }
+
+    /**
+     * Whether the file is a Citrus test, such as test/orders.citrus.it.yaml: 
camel test run runs it, and no integration
+     * loads a route from it. A Citrus test is named name.citrus.yaml, 
name.citrus.it.yaml or another
+     * name.citrus.*.yaml, the same in .xml or .groovy, or is a Cucumber 
.feature. A Java or XML file with a test name,
+     * such as OrderTest.java or OrderIT.xml, is a Citrus test when it uses 
Citrus, as a route can have such a name too.
+     *
+     * @param file the file, read only for a Java or XML test name
+     */
+    public static boolean isCitrusTest(Path file) {
+        Path fileName = file.getFileName();
+        if (fileName == null) {
+            return false;
+        }
+        String name = fileName.toString();
+        if (CITRUS_TEST.matcher(name.toLowerCase(Locale.ROOT)).matches()) {
+            return true;
+        }
+        if (CITRUS_TEST_CLASS.matcher(name).matches() && 
Files.isRegularFile(file)) {
+            try {
+                // the package org.citrusframework, or the namespace of a test 
in the XML DSL of Citrus
+                return Files.readString(file).contains("citrusframework");
+            } catch (IOException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The Citrus tests of a folder that holds nothing else camel run would 
load, such as the test folder of a project.
+     * Empty when the folder holds no Citrus test, or a route or another 
source, a Camel application.properties, or a
+     * subfolder (which can hold routes). Hidden files, data files and the 
other properties files, such as
+     * citrus-application.properties, do not count.
+     *
+     * @param dir the folder to run (camel run test/, or --source-dir)
+     */
+    public static List<Path> citrusTestsOnly(Path dir) {
+        List<Path> entries;
+        try (Stream<Path> list = Files.list(dir)) {
+            entries = list.sorted().toList();
+        } catch (IOException e) {
+            return List.of();
+        }
+        List<Path> tests = new ArrayList<>();
+        for (Path entry : entries) {
+            String name = entry.getFileName().toString();
+            if (name.startsWith(".")) {
+                continue;
+            }
+            if (Files.isDirectory(entry)) {
+                return List.of();
+            }
+            if (isCitrusTest(entry)) {
+                tests.add(entry);
+            } else if (name.endsWith(".properties")) {
+                // the configuration of camel run, which can name the routes 
to load (camel.main.routesIncludePattern)
+                if (name.equals("application.properties") || 
name.startsWith("application-")) {
+                    return List.of();
+                }
+            } else if (isGroovyFile(name) || loadsRoutes(entry)) {
+                return List.of();
+            }
+        }
+        return tests;
+    }
+
+    /** Whether camel run would load the file as a route or another source; a 
file it cannot read is taken as one. */
+    private static boolean loadsRoutes(Path file) {
+        try {
+            return knownFile(file.toString());
+        } catch (Exception e) {
+            return true;
+        }
+    }
+
+    /**
+     * What a run says of the Citrus tests among its files: they are not 
routes, and camel test run runs them.
+     *
+     * @param tests the Citrus tests, as given to run
+     */
+    public static String citrusTestMessage(List<String> tests) {
+        String files = String.join(" ", tests);
+        if (tests.size() == 1) {
+            return files + " is a Citrus test, not a route: run it with camel 
test run " + files;
+        }
+        return String.join(", ", tests) + " are Citrus tests, not routes: run 
them with camel test run " + files;
+    }
+
+    /**
+     * The line a run prints for a Citrus test given with routes: the test is 
left out, and the routes run.
+     *
+     * @param test the Citrus test, as given to run
+     */
+    public static String citrusTestSkipMessage(String test) {
+        return "Skipping " + test + ": a Citrus test, not a route (run it with 
camel test run " + test + ")";
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/RunFilesTest.java
 
b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/RunFilesTest.java
new file mode 100644
index 000000000000..c35956270ddd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-run/src/test/java/org/apache/camel/dsl/jbang/run/RunFilesTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.run;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A Citrus test is not a file to run: camel test run runs it, and camel run 
would load no route from it.
+ */
+class RunFilesTest {
+
+    @TempDir
+    Path dir;
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "orders.citrus.yaml", "orders.citrus.it.yaml", 
"orders.citrus.test.yaml", "orders.citrus-it.yaml",
+            "orders.citrus-test.yaml", "orders.citrus.it.yml", 
"orders.citrus.xml", "orders.citrus.it.xml",
+            "orders.citrus.groovy", "orders.citrus-it.groovy", 
"orders.feature", "orders.citrus.feature" })
+    void aCitrusTestByItsName(String name) {
+        // the names Citrus runs as a test, which no route has: the file is 
not read, so it need not be there
+        
assertThat(RunFiles.isCitrusTest(dir.resolve("test").resolve(name))).isTrue();
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "orders.camel.yaml", "orders.yaml", "citrus.yaml", 
"orders.camel.xml", "Orders.java", "orders.groovy",
+            "citrus-application.properties", "orders.citrus.properties" })
+    void notACitrusTest(String name) {
+        assertThat(RunFiles.isCitrusTest(dir.resolve(name))).isFalse();
+    }
+
+    @Test
+    void aJavaOrXmlTestIsACitrusTestWhenItUsesCitrus() throws Exception {
+        Path java = Files.writeString(dir.resolve("OrdersIT.java"),
+                "import org.citrusframework.TestCaseRunner;\n\npublic class 
OrdersIT implements Runnable {\n"
+                                                                    + "    
public void run() {\n    }\n}\n");
+        Path xml = Files.writeString(dir.resolve("OrdersTest.xml"),
+                "<test name=\"OrdersTest\" 
xmlns=\"http://citrusframework.org/schema/xml/testcase\";>\n"
+                                                                    + "  
<actions>\n    <echo message=\"hi\"/>\n  </actions>\n</test>\n");
+        assertThat(RunFiles.isCitrusTest(java)).isTrue();
+        assertThat(RunFiles.isCitrusTest(xml)).isTrue();
+
+        // a route can have such a name as well
+        Path route = Files.writeString(dir.resolve("LoadTest.java"),
+                "import org.apache.camel.builder.RouteBuilder;\n\npublic class 
LoadTest extends RouteBuilder {\n"
+                                                                     + "    
public void configure() {\n"
+                                                                     + "       
 from(\"timer:load\").to(\"log:load\");\n    }\n}\n");
+        Path routes = Files.writeString(dir.resolve("LoadIT.xml"),
+                "<routes xmlns=\"http://camel.apache.org/schema/xml-io\";>\n  
<route>\n    <from uri=\"timer:load\"/>\n"
+                                                                   + "    <to 
uri=\"log:load\"/>\n  </route>\n</routes>\n");
+        assertThat(RunFiles.isCitrusTest(route)).isFalse();
+        assertThat(RunFiles.isCitrusTest(routes)).isFalse();
+        // nothing to read
+        
assertThat(RunFiles.isCitrusTest(dir.resolve("MissingIT.java"))).isFalse();
+    }
+
+    @Test
+    void theMessageSaysToRunTheTestWithCamelTestRun() {
+        
assertThat(RunFiles.citrusTestMessage(List.of("test/orders.citrus.it.yaml")))
+                .isEqualTo("test/orders.citrus.it.yaml is a Citrus test, not a 
route: run it with camel test run"
+                           + " test/orders.citrus.it.yaml");
+        
assertThat(RunFiles.citrusTestMessage(List.of("test/orders.citrus.it.yaml", 
"test/orders.feature")))
+                .isEqualTo("test/orders.citrus.it.yaml, test/orders.feature 
are Citrus tests, not routes: run them with"
+                           + " camel test run test/orders.citrus.it.yaml 
test/orders.feature");
+        // a test given with routes is left out
+        assertThat(RunFiles.citrusTestSkipMessage("orders.citrus.it.yaml"))
+                .isEqualTo("Skipping orders.citrus.it.yaml: a Citrus test, not 
a route (run it with camel test run"
+                           + " orders.citrus.it.yaml)");
+    }
+
+    @Test
+    void aFolderOfOnlyCitrusTests() throws Exception {
+        // the test folder of a project: its tests, with the files a test 
reads, and its Citrus configuration
+        Path test = Files.createDirectories(dir.resolve("test"));
+        Path orders = Files.writeString(test.resolve("orders.citrus.it.yaml"), 
"name: orders-test\nactions: []\n");
+        Path refunds = 
Files.writeString(test.resolve("refunds.citrus.it.yaml"), "name: 
refunds-test\nactions: []\n");
+        Files.writeString(test.resolve("citrus-application.properties"), 
"citrus.camel.jbang.version=4.24.0\n");
+        Files.writeString(test.resolve("sample-order.json"), "{\"orderId\": 
\"ORD-1\"}\n");
+        Files.writeString(test.resolve("compose.yaml"), "services:\n  
broker:\n    image: apache/artemis\n");
+        Files.writeString(test.resolve(".gitignore"), "target\n");
+        assertThat(RunFiles.citrusTestsOnly(test)).containsExactly(orders, 
refunds);
+
+        // a folder that holds what camel run loads: a route, the Camel 
configuration, or a subfolder
+        Path route = Files.writeString(test.resolve("orders.camel.yaml"), "- 
from:\n    uri: timer:tick\n");
+        assertThat(RunFiles.citrusTestsOnly(test)).isEmpty();
+        Files.delete(route);
+        Path props = Files.writeString(test.resolve("application.properties"), 
"camel.server.port=8080\n");
+        assertThat(RunFiles.citrusTestsOnly(test)).isEmpty();
+        Files.delete(props);
+        Files.createDirectories(test.resolve("routes"));
+        assertThat(RunFiles.citrusTestsOnly(test)).isEmpty();
+
+        // a project with a test folder, a folder without tests, and a folder 
that is not there
+        assertThat(RunFiles.citrusTestsOnly(dir)).isEmpty();
+        
assertThat(RunFiles.citrusTestsOnly(Files.createDirectories(dir.resolve("empty")))).isEmpty();
+        assertThat(RunFiles.citrusTestsOnly(dir.resolve("missing"))).isEmpty();
+    }
+}

Reply via email to