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 07c54cb62eae CAMEL-23688: camel-jbang - Tests of more commands, and 
the bugs they found (#27934)
07c54cb62eae is described below

commit 07c54cb62eaed250041226023fb97db6e20c3309
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 10:51:47 2026 +0200

    CAMEL-23688: camel-jbang - Tests of more commands, and the bugs they found 
(#27934)
    
    Tests for commands and helpers that had none: camel log (tail, grep, find,
    since, startup, prefix, a log named after the integration), trace (status,
    start, stop, clear), cmd send (the message, the exit code of the answer, the
    reply file), eval expression in a running integration, cmd sql, cmd
    heap-histogram, get datasource, get sql-trace, infra run and stop, jolokia, 
the name
    of an integration (ProcessHelper), the user configuration as the default of
    the options (placeholders, local before global, not for the plugin
    commands), and the version comparison.
    
    The bugs they found:
    - cmd send --reply failed with a ClassCastException when the exchange failed
      with an exception without a message, such as a NullPointerException.
    - get source failed for routes without a source location, and for an answer
      without routes.
    - Versions compared as text: 3.27.3.1 came after 3.27.4 (the Quarkus
      platform has four parts), and 4.24.10-SNAPSHOT came after 4.24.10; a
      pre-release (-SNAPSHOT, -RC1, -M2) now comes before its release and
      another qualifier after it, as in Maven.
    - jolokia exited 1 for a name that matches no integration or several (the
      second on stdout); now 3, as hawtio does.
    - config set =value stored a value with an empty key: a parameter without a
      key or a value exits 2 with the error on stderr, and creates no file.
    - dependency list with an unknown --output exits 2.
    - cmd memory-leak --min-size=10B (and a value that is not a size) turned the
      filter off: 10B is 10 bytes, and a value that is not a size exits 2.
    - camel run of a GitHub repository or a gist skipped .camel.xml files.
    - cmd heap-dump printed the size with the decimal separator of the locale.
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  17 +++
 .../dsl/jbang/core/commands/DependencyList.java    |   4 +-
 .../jbang/core/commands/action/CamelHeapDump.java  |  10 +-
 .../core/commands/action/CamelMemoryLeak.java      |  17 ++-
 .../core/commands/action/CamelSourceAction.java    |  11 +-
 .../dsl/jbang/core/commands/config/ConfigSet.java  |  10 +-
 .../dsl/jbang/core/commands/process/Jolokia.java   |  15 +-
 .../camel/dsl/jbang/core/common/ActionClient.java  |  16 +-
 .../camel/dsl/jbang/core/common/GistHelper.java    |   3 +-
 .../camel/dsl/jbang/core/common/GitHubHelper.java  |  17 ++-
 .../camel/dsl/jbang/core/common/VersionHelper.java | 124 ++++++++-------
 .../jbang/core/commands/DependencyListTest.java    |  12 ++
 .../core/commands/UserConfigDefaultsTest.java      | 124 +++++++++++++++
 .../core/commands/action/CamelHeapDumpTest.java    |  43 ++++++
 .../commands/action/CamelHeapHistogramTest.java    | 106 +++++++++++++
 .../core/commands/action/CamelLogActionTest.java   | 167 +++++++++++++++++++++
 .../core/commands/action/CamelMemoryLeakTest.java  |  49 ++++++
 .../commands/action/CamelSendActionResultTest.java | 142 ++++++++++++++++++
 .../action/CamelSourceActionRoutesTest.java        |  83 ++++++++++
 .../commands/action/CamelSqlQueryActionTest.java   |  98 ++++++++++++
 .../core/commands/action/CamelTraceActionTest.java | 117 +++++++++++++++
 .../commands/action/EvalExpressionResultTest.java  |  92 ++++++++++++
 .../jbang/core/commands/config/ConfigSetTest.java  |  27 ++++
 .../core/commands/infra/InfraRunSelectionTest.java |  61 ++++++++
 .../jbang/core/commands/infra/InfraStopTest.java   | 122 +++++++++++++++
 .../jbang/core/commands/process/JolokiaTest.java   |  71 +++++++++
 .../core/commands/process/ListDataSourceTest.java  | 120 +++++++++++++++
 .../core/commands/process/ListSqlTraceTest.java    | 124 +++++++++++++++
 .../dsl/jbang/core/common/ActionClientTest.java    |   9 ++
 .../dsl/jbang/core/common/GitHubHelperTest.java    |  43 ++++++
 .../dsl/jbang/core/common/ProcessHelperTest.java   | 104 +++++++++++++
 .../dsl/jbang/core/common/VersionHelperTest.java   |  78 ++++++++++
 32 files changed, 1931 insertions(+), 105 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 d560b88d9dda..3897a0498f18 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
@@ -454,6 +454,23 @@ or follows, like Enter does.
 * A plugin that cannot be loaded ends the command with exit code `1` and the 
error, as before, but no longer exits the
   JVM while the plugins load, which ended a program that embeds the CLI.
 
+=== camel-jbang - exit codes of config set, dependency list, jolokia and cmd 
memory-leak
+
+* `camel jolokia` with a name or pid that matches no running integration, or 
more than one, exits with `3`, as
+  `camel hawtio` does (it exited with `1`, and printed the second case on 
stdout).
+* `camel config set` without a key (`=value` stored a value with an empty key) 
or without a value exits with `2`, and
+  prints the error on stderr.
+* `camel dependency list` with an unknown `--output` exits with `2`.
+* `camel cmd memory-leak --min-size` takes a size in bytes such as `10B`, and 
a value that is not a size exits with `2`
+  (both turned the filter off).
+* `camel cmd send --reply` to a route that fails with an exception without a 
message, such as a `NullPointerException`,
+  prints the failed exchange and exits with `1`, instead of failing with a 
`ClassCastException`.
+* `camel get source` of an integration with routes without a source location, 
such as routes added by a
+  `RouteBuilder`, no longer fails.
+* Versions with four parts, such as the Quarkus platform version `3.27.3.1`, 
compare as versions in the version checks
+  of the CLI (`3.27.3.1` came after `3.27.4`), and a `-SNAPSHOT` always comes 
before its release, as in Maven
+  (`4.24.10-SNAPSHOT` came after `4.24.10`).
+
 === camel-jpa, camel-hazelcast - an exchange marked rollback only is not 
consumed
 
 When a route marks the exchange rollback only (for example with 
`markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyList.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyList.java
index 41aed5ba0de9..25c6b1fe093f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyList.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/DependencyList.java
@@ -34,6 +34,7 @@ import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.NodeList;
 
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.RuntimeType;
 import org.apache.camel.dsl.jbang.core.common.XmlHelper;
@@ -86,8 +87,7 @@ public class DependencyList extends Export {
     @Override
     protected Integer export() throws Exception {
         if (!"gav".equals(output) && !"maven".equals(output) && 
!"jbang".equals(output)) {
-            printer().printErr("--output must be either gav or maven, was: " + 
output);
-            return 1;
+            throw CliException.usage("--output must be gav, maven or jbang, 
was: " + output);
         }
 
         exportBaseDir = Path.of(".");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java
index 5fa9a2d3bf1b..bd907f5ac4d4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDump.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.action;
 
+import java.util.Locale;
+
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.JsonOutput;
@@ -85,13 +87,13 @@ public class CamelHeapDump extends ActionBaseCommand {
         return 0;
     }
 
-    private static String formatSize(long bytes) {
+    static String formatSize(long bytes) {
         if (bytes >= 1024 * 1024 * 1024) {
-            return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 
1024.0));
+            return String.format(Locale.ROOT, "%.1f GB", bytes / (1024.0 * 
1024.0 * 1024.0));
         } else if (bytes >= 1024 * 1024) {
-            return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
+            return String.format(Locale.ROOT, "%.1f MB", bytes / (1024.0 * 
1024.0));
         } else if (bytes >= 1024) {
-            return String.format("%.1f KB", bytes / 1024.0);
+            return String.format(Locale.ROOT, "%.1f KB", bytes / 1024.0);
         } else {
             return bytes + " bytes";
         }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
index b2bfc3e63ca2..f208643e0881 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeak.java
@@ -28,6 +28,7 @@ import com.github.freva.asciitable.HorizontalAlign;
 import com.github.freva.asciitable.OverflowBehaviour;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.ActionClient;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
 import org.apache.camel.util.json.JsonArray;
@@ -96,6 +97,8 @@ public class CamelMemoryLeak extends ActionBaseCommand {
 
     @Override
     public Integer doCall() throws Exception {
+        // a --min-size that is not a size is a usage error, before the 
integration is asked anything
+        parseSize(minSize);
         long pid = findOne(name).pid();
 
         if (start && "dual".equalsIgnoreCase(mode)) {
@@ -437,26 +440,30 @@ public class CamelMemoryLeak extends ActionBaseCommand {
         }
     }
 
+    /**
+     * The number of bytes of a size such as 1024, 10B, 10KB, 1.5MB or 2G.
+     *
+     * @throws CliException (exit code 2) when the value is not a size
+     */
     static long parseSize(String value) {
-        if (value == null || value.isEmpty() || "0".equals(value)) {
+        if (value == null || value.isBlank()) {
             return 0;
         }
         String v = value.trim().toUpperCase(Locale.US);
         long multiplier = 1;
         if (v.endsWith("GB") || v.endsWith("G")) {
             multiplier = 1024L * 1024 * 1024;
-            v = v.replaceAll("[GMK]B?$", "");
         } else if (v.endsWith("MB") || v.endsWith("M")) {
             multiplier = 1024L * 1024;
-            v = v.replaceAll("[GMK]B?$", "");
         } else if (v.endsWith("KB") || v.endsWith("K")) {
             multiplier = 1024;
-            v = v.replaceAll("[GMK]B?$", "");
         }
+        // without the unit, which can also be B for bytes
+        v = v.replaceAll("[GMK]?B?$", "");
         try {
             return (long) (Double.parseDouble(v) * multiplier);
         } catch (NumberFormatException e) {
-            return 0;
+            throw CliException.usage("--min-size must be a size such as 1024, 
10KB or 1MB, was: " + value);
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceAction.java
index 77465976cb48..5a658552522a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceAction.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.action;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.common.JsonOutput;
@@ -72,7 +73,7 @@ public class CamelSourceAction extends ActionBaseCommand {
         JsonObject jo = executeAction(process, "source", root -> {
             root.put("filter", "*");
         });
-        JsonArray arr = (JsonArray) jo.get("routes");
+        JsonArray arr = jo.get("routes") instanceof JsonArray routes ? routes 
: new JsonArray();
         for (int i = 0; i < arr.size(); i++) {
             JsonObject o = (JsonObject) arr.get(i);
             Row row = new Row();
@@ -129,7 +130,9 @@ public class CamelSourceAction extends ActionBaseCommand {
         }
         switch (s) {
             case "name":
-                return o1.location.compareToIgnoreCase(o2.location) * negate;
+                // a route without a source location sorts first
+                return (o1.location != null ? o1.location : 
"").compareToIgnoreCase(o2.location != null ? o2.location : "")
+                       * negate;
             default:
                 return 0;
         }
@@ -177,12 +180,12 @@ public class CamelSourceAction extends ActionBaseCommand {
 
             Row row = (Row) o;
 
-            return location.equals(row.location);
+            return Objects.equals(location, row.location);
         }
 
         @Override
         public int hashCode() {
-            return location.hashCode();
+            return Objects.hashCode(location);
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSet.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSet.java
index 2a8c748f4208..2feb0e57693d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSet.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSet.java
@@ -20,6 +20,7 @@ import java.io.IOException;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.StringHelper;
 import picocli.CommandLine;
@@ -48,12 +49,11 @@ public class ConfigSet extends CamelCommand {
     }
 
     private int setConfiguration(boolean local) throws IOException {
-        CommandLineHelper.createPropertyFile(local);
-
-        if (configuration.split("=").length == 1) {
-            printer().println("Configuration parameter not in key=value 
format");
-            return 1;
+        String name = StringHelper.before(configuration, "=");
+        if (name == null || name.isBlank() || configuration.split("=").length 
== 1) {
+            throw CliException.usage("Configuration parameter not in key=value 
format: " + configuration);
         }
+        CommandLineHelper.createPropertyFile(local);
 
         CommandLineHelper.loadProperties(properties -> {
             String key = StringHelper.before(configuration, "=").trim();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Jolokia.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Jolokia.java
index 1c6490dc029a..675e9422d080 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Jolokia.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/Jolokia.java
@@ -22,11 +22,11 @@ import java.lang.reflect.Field;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.List;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
 import org.jolokia.jvmagent.client.command.CommandDispatcher;
 import org.jolokia.jvmagent.client.util.OptionsAndArgs;
 import org.jolokia.jvmagent.client.util.PlatformUtils;
@@ -61,17 +61,8 @@ public class Jolokia extends ProcessBaseCommand {
 
     @Override
     public Integer doCall() throws Exception {
-        List<Long> pids = findPids(name);
-        if (pids.isEmpty()) {
-            printer().printErr("No running Camel integration matches: " + 
name);
-            return 1;
-        } else if (pids.size() > 1) {
-            printer().println("Name or pid " + name + " matches " + pids.size()
-                              + " running Camel integrations. Specify a name 
or PID that matches exactly one.");
-            return 1;
-        }
-
-        this.pid = pids.get(0);
+        // exactly one running integration, or exit 3 (as camel hawtio does 
with a name too)
+        this.pid = ProcessFinder.requireOne(name).pid();
         int exitCode;
         try {
             OptionsAndArgs options;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
index 934246d6106b..06744756dda6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ActionClient.java
@@ -289,14 +289,22 @@ public final class ActionClient {
     }
 
     private static String messageOf(JsonObject reply) {
-        if (reply.get("exception") instanceof JsonObject exception && 
exception.getString("message") != null) {
-            return exception.getString("message");
+        if (reply.get("exception") instanceof JsonObject exception) {
+            // an exception without a message, such as a NullPointerException, 
is known by its type
+            String message
+                    = exception.getString("message") != null ? 
exception.getString("message") : exception.getString("type");
+            if (message != null) {
+                return message;
+            }
         }
         if (reply.get("error") instanceof String error && !error.isBlank()) {
             return error;
         }
-        String message = reply.getString("message");
-        return message != null && !message.isBlank() ? message : "Action 
failed";
+        // the message of the answer of camel cmd send is the reply message (a 
JSON object), not an error
+        if (reply.get("message") instanceof String message && 
!message.isBlank()) {
+            return message;
+        }
+        return "Action failed";
     }
 
     private static JsonObject request(String action, Consumer<JsonObject> 
options) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GistHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GistHelper.java
index bc89f5639642..19d8bbbe0ae9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GistHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GistHelper.java
@@ -103,8 +103,7 @@ public final class GistHelper {
                     String u = asGistSingleUrl(rawUrl);
                     properties.add(u);
                 } else if (routes != null) {
-                    if ("java".equalsIgnoreCase(ext) || 
"xml".equalsIgnoreCase(ext)
-                            || "yaml".equalsIgnoreCase(ext) || 
"camel.yaml".equalsIgnoreCase(ext)) {
+                    if (GitHubHelper.isRouteExtension(ext)) {
                         String rawUrl = c.get("raw_url").asText();
                         String u = asGistSingleUrl(rawUrl);
                         routes.add(u);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GitHubHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GitHubHelper.java
index caf0b2ace047..3634c2977da0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GitHubHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/GitHubHelper.java
@@ -22,6 +22,7 @@ import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.time.Duration;
+import java.util.Locale;
 import java.util.StringJoiner;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -119,6 +120,16 @@ public final class GitHubHelper {
         resolveGithubAsRawFiles(url, wildcard, routes, kamelets, properties, 
all);
     }
 
+    /**
+     * Whether a file with the extension (from FileUtil.onlyExt(name, false), 
such as camel.yaml) has routes.
+     */
+    static boolean isRouteExtension(String ext) {
+        return ext != null && switch (ext.toLowerCase(Locale.ROOT)) {
+            case "java", "xml", "camel.xml", "yaml", "camel.yaml" -> true;
+            default -> false;
+        };
+    }
+
     private static void addSingleFileUrl(
             String url, StringJoiner routes, StringJoiner kamelets, 
StringJoiner properties,
             StringJoiner all) {
@@ -131,8 +142,7 @@ public final class GitHubHelper {
         } else if (properties != null && "properties".equalsIgnoreCase(ext)) {
             properties.add(ghUrl);
         } else if (routes != null) {
-            if ("java".equalsIgnoreCase(ext) || "xml".equalsIgnoreCase(ext)
-                    || "yaml".equalsIgnoreCase(ext) || 
"camel.yaml".equalsIgnoreCase(ext)) {
+            if (isRouteExtension(ext)) {
                 routes.add(ghUrl);
             }
         }
@@ -164,8 +174,7 @@ public final class GitHubHelper {
                         String u = asGithubSingleUrl(htmlUrl);
                         properties.add(u);
                     } else if (routes != null) {
-                        if ("java".equalsIgnoreCase(ext) || 
"xml".equalsIgnoreCase(ext)
-                                || "yaml".equalsIgnoreCase(ext) || 
"camel.yaml".equalsIgnoreCase(ext)) {
+                        if (isRouteExtension(ext)) {
                             String htmlUrl = c.get("html_url").asText();
                             String u = asGithubSingleUrl(htmlUrl);
                             routes.add(u);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/VersionHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/VersionHelper.java
index 0cd53f4e7435..893856a75ece 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/VersionHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/VersionHelper.java
@@ -19,11 +19,20 @@ package org.apache.camel.dsl.jbang.core.common;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-
-import org.apache.camel.util.StringHelper;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public final class VersionHelper {
 
+    private static final Pattern LEADING_NUMBER = 
Pattern.compile("^\\d{1,18}");
+    // the qualifiers of a version before its release, as in Maven: 
4.24.0-SNAPSHOT, -RC1, -M2, -beta1
+    private static final Pattern PRE_RELEASE
+            = 
Pattern.compile("^[-.]?(snapshot|alpha|beta|milestone|preview|rc|cr|ea|m|a|b)(?=\\d|[-.]|$)",
+                    Pattern.CASE_INSENSITIVE);
+
     private VersionHelper() {
     }
 
@@ -61,73 +70,62 @@ public final class VersionHelper {
         return compare(source, inclusive) >= 0 && compare(source, exclusive) < 
0;
     }
 
+    /**
+     * Compares two versions part by part as numbers, such as 3.27.3.1 before 
3.27.4, and 4.100 after 4.99. With the
+     * same numbers, a pre-release (such as 4.24.0-SNAPSHOT, -RC1 or -M2) 
comes before the release, and another
+     * qualifier (such as a build number) after it, as in Maven.
+     *
+     * @return a negative number, zero or a positive number as the source is 
before, the same as, or after the target
+     *         (zero when one of them is missing)
+     */
     public static int compare(String source, String target) {
         if (source == null || target == null || source.isBlank() || 
target.isBlank()) {
             return 0;
         }
-        String s1 = StringHelper.before(source, ".");
-        String s2 = StringHelper.after(source, ".");
-        if (s1 == null) {
-            s1 = StringHelper.before(source, ",");
-            s2 = StringHelper.after(source, ",");
-        }
-        String t1 = StringHelper.before(target, ".");
-        String t2 = StringHelper.after(target, ".");
-        if (t1 == null) {
-            t1 = StringHelper.before(target, ",");
-            t2 = StringHelper.after(target, ",");
-        }
-        String s3 = StringHelper.after(s2, ".");
-        if (s3 != null) {
-            s2 = StringHelper.before(s2, ".");
-        } else {
-            s3 = "";
-        }
-        String t3 = StringHelper.after(t2, ".");
-        if (t3 != null) {
-            t2 = StringHelper.before(t2, ".");
-        } else {
-            t3 = "";
-        }
-        // avoid NPE
-        if (s1 == null) {
-            s1 = source;
-        }
-        if (s2 == null) {
-            s2 = "";
-        }
-        if (t1 == null) {
-            t1 = target;
-        }
-        if (t2 == null) {
-            t2 = "";
-        }
-        // convert to 2-digit numbers
-        if (s1.length() < 2) {
-            s1 = "0" + s1;
-        }
-        if (s2.length() < 2) {
-            s2 = "0" + s2;
-        }
-        if (s2.length() < 2) {
-            s2 = "0" + s2;
-        }
-        if (s3.length() < 2) {
-            s3 = "0" + s3;
-        }
-        if (t1.length() < 2) {
-            t1 = "0" + t1;
-        }
-        if (t2.length() < 2) {
-            t2 = "0" + t2;
-        }
-        if (t3.length() < 2) {
-            t3 = "0" + t3;
+        List<Long> s = new ArrayList<>();
+        String sq = parse(source, s);
+        List<Long> t = new ArrayList<>();
+        String tq = parse(target, t);
+        for (int i = 0; i < Math.max(s.size(), t.size()); i++) {
+            long a = i < s.size() ? s.get(i) : 0;
+            long b = i < t.size() ? t.get(i) : 0;
+            if (a != b) {
+                return Long.compare(a, b);
+            }
         }
+        int rank = Integer.compare(qualifierRank(sq), qualifierRank(tq));
+        return rank != 0 ? rank : sq.compareToIgnoreCase(tq);
+    }
 
-        String s = s1 + s2 + s3;
-        String t = t1 + t2 + t3;
-        return s.compareTo(t);
+    /**
+     * Where a qualifier sorts: a pre-release before the release, and any 
other qualifier after it.
+     */
+    private static int qualifierRank(String qualifier) {
+        if (qualifier.isEmpty()) {
+            return 1;
+        }
+        return PRE_RELEASE.matcher(qualifier).lookingAt() ? 0 : 2;
+    }
+
+    /**
+     * The numbers of the version (separated by dots or commas), and what 
follows them, such as -SNAPSHOT.
+     */
+    private static String parse(String version, List<Long> numbers) {
+        String[] parts = version.trim().split("[.,]");
+        for (int i = 0; i < parts.length; i++) {
+            Matcher m = LEADING_NUMBER.matcher(parts[i]);
+            if (!m.find()) {
+                return String.join(".", Arrays.copyOfRange(parts, i, 
parts.length));
+            }
+            numbers.add(Long.parseLong(m.group()));
+            if (m.end() < parts[i].length()) {
+                // such as 0-SNAPSHOT
+                String rest = parts[i].substring(m.end());
+                return i + 1 < parts.length
+                        ? rest + "." + String.join(".", 
Arrays.copyOfRange(parts, i + 1, parts.length)) : rest;
+            }
+        }
+        return "";
     }
 
     public static String getMajorMinorVersion(String version) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyListTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyListTest.java
index 000f260dab67..3aa581256e6a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyListTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyListTest.java
@@ -23,11 +23,14 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Stream;
 
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.RuntimeType;
 import org.apache.camel.util.FileUtil;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -72,6 +75,15 @@ class DependencyListTest extends CamelCommandBaseTestSupport 
{
         }
     }
 
+    @Test
+    public void shouldRejectUnknownOutput() throws Exception {
+        DependencyList command = createCommand(RuntimeType.main, new String[] 
{ "classpath:route.yaml" }, "--output=pom");
+
+        CliException e = Assertions.assertThrows(CliException.class, 
command::doCall);
+        Assertions.assertEquals(CliExitCode.USAGE, e.getExitCode());
+        Assertions.assertEquals("--output must be gav, maven or jbang, was: 
pom", e.getMessage());
+    }
+
     private DependencyList createCommand(RuntimeType rt, String[] files, 
String... args) {
         DependencyList command = new DependencyList(new 
CamelJBangMain().withPrinter(printer));
         CommandLine.populateCommand(command, "--gav=examples:route:1.0.0", 
"--dir=" + workingDir, "--quiet",
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/UserConfigDefaultsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/UserConfigDefaultsTest.java
new file mode 100644
index 000000000000..b82e474de6c7
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/UserConfigDefaultsTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands;
+
+import org.apache.camel.dsl.jbang.core.commands.config.BaseConfigTestSupport;
+import org.apache.camel.dsl.jbang.core.commands.plugin.PluginCommand;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The user configuration (camel config set) gives the default values of the 
options of the commands.
+ */
+class UserConfigDefaultsTest extends BaseConfigTestSupport {
+
+    @Test
+    void configurationIsTheDefaultOfEveryCommandWithTheOption() throws 
Exception {
+        UserConfigHelper.createUserConfig("sort=name");
+
+        assertThat(run("echo")).isEqualTo("sort=name repos=null");
+        assertThat(run("other")).isEqualTo("sort=name repos=null");
+    }
+
+    @Test
+    void optionOnTheCommandLineWins() throws Exception {
+        UserConfigHelper.createUserConfig("sort=name");
+
+        assertThat(run("echo", "--sort=age")).isEqualTo("sort=age repos=null");
+    }
+
+    @Test
+    void configurationOfOneCommand() throws Exception {
+        UserConfigHelper.createUserConfig("camel.echo.sort=age\nsort=name");
+
+        assertThat(run("echo")).isEqualTo("sort=age repos=null");
+        assertThat(run("other")).isEqualTo("sort=name repos=null");
+    }
+
+    @Test
+    void withoutConfigurationTheOptionHasItsOwnDefault() throws Exception {
+        UserConfigHelper.createUserConfig("");
+
+        assertThat(run("echo")).isEqualTo("sort=pid repos=null");
+    }
+
+    @Test
+    void placeholderOfTheConfiguredValue() throws Exception {
+        UserConfigHelper.createUserConfig("repos=https://repo.example.com/ga";);
+
+        assertThat(run("echo", 
"--repos=#repos,https://other.example.com/maven";))
+                .isEqualTo("sort=pid 
repos=https://repo.example.com/ga,https://other.example.com/maven";);
+    }
+
+    @Test
+    void localConfigurationTakesPrecedence() throws Exception {
+        UserConfigHelper.createUserConfig("sort=name");
+        UserConfigHelper.createUserConfig("sort=age", true);
+
+        assertThat(run("echo")).isEqualTo("sort=age repos=null");
+    }
+
+    @Test
+    void pluginCommandsDoNotUseTheConfiguration() throws Exception {
+        UserConfigHelper.createUserConfig("sort=name");
+
+        assertThat(run("plugin", "echo")).isEqualTo("sort=pid repos=null");
+    }
+
+    private static String run(String... args) {
+        StringPrinter out = new StringPrinter();
+        CamelJBangMain main = new CamelJBangMain().withPrinter(out);
+        CommandLine commandLine = new CommandLine(main)
+                .addSubcommand("echo", new CommandLine(new EchoCommand(main)))
+                .addSubcommand("other", new CommandLine(new EchoCommand(main)))
+                .addSubcommand("plugin", new CommandLine(new 
PluginCommand(main))
+                        .addSubcommand("echo", new CommandLine(new 
EchoCommand(main))));
+        CommandLineHelper.augmentWithUserConfiguration(commandLine);
+
+        assertThat(commandLine.execute(args)).isZero();
+        return out.getStdout();
+    }
+
+    // no name of its own: it is the name it is added with
+    static class EchoCommand extends CamelCommand {
+
+        @CommandLine.Option(names = { "--sort" }, defaultValue = "pid")
+        String sort;
+
+        @CommandLine.Option(names = { "--repos" })
+        String repos;
+
+        EchoCommand(CamelJBangMain main) {
+            super(main);
+        }
+
+        @Override
+        protected boolean disarrangeLogging() {
+            return false;
+        }
+
+        @Override
+        public Integer doCall() {
+            printer().println("sort=" + sort + " repos=" + repos);
+            return 0;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDumpTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDumpTest.java
new file mode 100644
index 000000000000..bb38dbdfae4f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapDumpTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.action;
+
+import java.util.Locale;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * camel cmd heap-dump prints the size of the dump the same way in every 
locale.
+ */
+class CamelHeapDumpTest {
+
+    @Test
+    void sizeIsTheSameInEveryLocale() {
+        Locale locale = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.GERMANY);
+            assertThat(CamelHeapDump.formatSize(512)).isEqualTo("512 bytes");
+            assertThat(CamelHeapDump.formatSize(1536)).isEqualTo("1.5 KB");
+            assertThat(CamelHeapDump.formatSize(5L * 1024 * 1024 / 
2)).isEqualTo("2.5 MB");
+            assertThat(CamelHeapDump.formatSize(3L * 1024 * 1024 * 
1024)).isEqualTo("3.0 GB");
+        } finally {
+            Locale.setDefault(locale);
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogramTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogramTest.java
new file mode 100644
index 000000000000..61db2fbeee5b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelHeapHistogramTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.action;
+
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.apache.camel.util.json.JsonArray;
+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.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * camel cmd heap-histogram: the classes the integration answers, filtered, 
sorted and cut to the top.
+ */
+@ExtendWith(MockitoExtension.class)
+class CamelHeapHistogramTest extends ActionCommandTestSupport {
+
+    @Test
+    void largestFirst() throws Exception {
+        assertThat(classNames()).containsExactly("byte[]", "java.lang.String", 
"org.apache.camel.Exchange");
+    }
+
+    @Test
+    void sortByInstancesOrName() throws Exception {
+        
assertThat(classNames("--sort=instances")).containsExactly("java.lang.String", 
"byte[]",
+                "org.apache.camel.Exchange");
+        assertThat(classNames("--sort=className")).containsExactly("byte[]", 
"java.lang.String",
+                "org.apache.camel.Exchange");
+        // smallest first
+        
assertThat(classNames("--sort=-bytes")).containsExactly("org.apache.camel.Exchange",
 "java.lang.String",
+                "byte[]");
+    }
+
+    @Test
+    void filterAndTop() throws Exception {
+        
assertThat(classNames("--filter=camel")).containsExactly("org.apache.camel.Exchange");
+        assertThat(classNames("--filter=non-jdk")).containsExactly("byte[]", 
"org.apache.camel.Exchange");
+        assertThat(classNames("--top=1")).containsExactly("byte[]");
+    }
+
+    @Test
+    void sizes() {
+        assertThat(CamelHeapHistogram.formatBytes(512)).isEqualTo("512 B");
+        assertThat(CamelHeapHistogram.formatBytes(1536)).isEqualTo("1.5 KB");
+        assertThat(CamelHeapHistogram.formatBytes(3L * 1024 * 1024 * 
1024)).isEqualTo("3.0 GB");
+        
assertThat(CamelHeapHistogram.formatNumber(1234567)).isEqualTo("1,234,567");
+    }
+
+    private List<String> classNames(String... args) throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        printer = new StringPrinter();
+        CamelHeapHistogram command = new CamelHeapHistogram(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, args);
+        command.name = "orders";
+        command.jsonOutput = true;
+
+        assertThat(callWithResponse(command, histogram())).isZero();
+
+        JsonObject answer = Jsoner.deserialize(printer.getStdout(), 
(JsonObject) null);
+        assertThat(answer.getLong("pid")).isEqualTo(TEST_PID);
+        JsonArray classes = answer.getCollection("classes");
+        return classes.stream().map(c -> ((JsonObject) 
c).getString("className")).toList();
+    }
+
+    private static JsonObject histogram() {
+        JsonObject answer = new JsonObject();
+        answer.put("totalInstances", 1600L);
+        answer.put("totalBytes", 90000L);
+        answer.put("classes", new JsonArray(
+                List.of(
+                        entry(1, "byte[]", 500, 60000),
+                        entry(2, "java.lang.String", 1000, 25000),
+                        entry(3, "org.apache.camel.Exchange", 100, 5000))));
+        return answer;
+    }
+
+    private static JsonObject entry(int num, String className, long instances, 
long bytes) {
+        JsonObject entry = new JsonObject();
+        entry.put("num", num);
+        entry.put("className", className);
+        entry.put("instances", instances);
+        entry.put("bytes", bytes);
+        return entry;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
new file mode 100644
index 000000000000..205e4b64330a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelLogActionTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * camel log: the lines of the log of an integration, without following it.
+ */
+@ExtendWith(MockitoExtension.class)
+class CamelLogActionTest extends ActionCommandTestSupport {
+
+    private static final DateTimeFormatter TIMESTAMP = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+
+    @Test
+    void printsTheLogOfTheIntegration() throws Exception {
+        writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-60, "Slow"), 
line(-1, "Failed"));
+
+        assertThat(callWithSingleProcess(command())).isZero();
+
+        // one integration: no prefix with its name
+        assertThat(lines()).hasSize(3).allMatch(l -> !l.contains("|"));
+        assertThat(lines().get(2)).endsWith(": Failed");
+    }
+
+    @Test
+    void tailPrintsTheLastLines() throws Exception {
+        writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-60, "Slow"), 
line(-1, "Failed"));
+
+        assertThat(callWithSingleProcess(command("--tail=2"))).isZero();
+
+        assertThat(lines()).hasSize(2);
+        assertThat(lines().get(0)).endsWith(": Slow");
+    }
+
+    @Test
+    void grepPrintsTheMatchingLines() throws Exception {
+        writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-60, "Slow"), 
line(-1, "Failed"));
+
+        // ignoring case, and only the message (after ---) matches
+        assertThat(callWithSingleProcess(command("--grep=FAILED"))).isZero();
+
+        assertThat(lines()).hasSize(1);
+        assertThat(lines().get(0)).endsWith(": Failed");
+    }
+
+    @Test
+    void findHighlightsButPrintsAllLines() throws Exception {
+        writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-60, "Slow"), 
line(-1, "Failed"));
+
+        assertThat(callWithSingleProcess(command("--find=slow"))).isZero();
+
+        assertThat(lines()).hasSize(3);
+        assertThat(printer.getStdout()).contains("\u001B[");
+    }
+
+    @Test
+    void sinceInSecondsOrWithAUnit() throws Exception {
+        writeLog(TEST_PID + ".log", line(-300, "Starting"), line(-60, "Slow"), 
line(-1, "Failed"));
+
+        assertThat(callWithSingleProcess(command("--since=30"))).isZero();
+        assertThat(lines()).hasSize(1);
+
+        printer = new StringPrinter();
+        assertThat(callWithSingleProcess(command("--since=2m"))).isZero();
+        assertThat(lines()).hasSize(2);
+    }
+
+    @Test
+    void startupPrintsUntilCamelHasStarted() throws Exception {
+        writeLog(TEST_PID + ".log", line(-300, "Starting"),
+                line(-299, "o.a.c.i.e.AbstractCamelContext",
+                        "Apache Camel 4.24.0 (orders) started in 120ms 
(build:30ms init:10ms start:80ms)"),
+                line(-1, "Failed"));
+
+        assertThat(callWithSingleProcess(command("--startup"))).isZero();
+
+        assertThat(lines()).hasSize(2);
+        assertThat(lines().get(1)).contains("started in 120ms");
+    }
+
+    @Test
+    void withoutTimestamp() throws Exception {
+        String line = line(-1, "Failed");
+        writeLog(TEST_PID + ".log", line);
+
+        
assertThat(callWithSingleProcess(command("--timestamp=false"))).isZero();
+
+        assertThat(lines().get(0).trim()).startsWith("INFO").endsWith(": 
Failed");
+        assertThat(lines().get(0)).doesNotContain(line.substring(0, 23));
+    }
+
+    @Test
+    void prefixWithTheNameOfTheIntegration() throws Exception {
+        writeLog(TEST_PID + ".log", line(-1, "Failed"));
+
+        assertThat(callWithSingleProcess(command("--prefix=true"))).isZero();
+
+        assertThat(lines().get(0)).startsWith("orders    | ");
+    }
+
+    @Test
+    void logNamedAfterTheIntegration() throws Exception {
+        // camel run --logging-file-name, or an older version of Camel
+        writeLog("orders.log", line(-1, "Failed"));
+
+        assertThat(callWithSingleProcess(command())).isZero();
+
+        assertThat(lines()).hasSize(1);
+    }
+
+    private CamelLogAction command(String... args) throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        CamelLogAction command = new CamelLogAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command,
+                Stream.concat(Stream.of("orders", "--follow=false", 
"--logging-color=false"), Stream.of(args))
+                        .toArray(String[]::new));
+        return command;
+    }
+
+    // without the highlighting of --find and --grep
+    private List<String> lines() throws Exception {
+        return printer.getLines().stream().map(l -> 
l.replaceAll("\u001B\\[[;\\d]*m", "")).toList();
+    }
+
+    private static void writeLog(String name, String... lines) throws 
Exception {
+        Files.write(CommandLineHelper.getCamelDir().resolve(name), 
List.of(lines));
+    }
+
+    private static String line(long seconds, String message) {
+        return line(seconds, "o.a.c.Test", message);
+    }
+
+    // the layout of the log of camel run
+    private static String line(long seconds, String logger, String message) {
+        return LocalDateTime.now().plusSeconds(seconds).format(TIMESTAMP) + "  
INFO " + TEST_PID + " --- [main] " + logger
+               + " : " + message;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeakTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeakTest.java
new file mode 100644
index 000000000000..4ab719b96614
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelMemoryLeakTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.action;
+
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel cmd memory-leak --min-size: the size of the samples to show.
+ */
+class CamelMemoryLeakTest {
+
+    @Test
+    void sizes() {
+        assertThat(CamelMemoryLeak.parseSize("0")).isZero();
+        assertThat(CamelMemoryLeak.parseSize("1024")).isEqualTo(1024);
+        assertThat(CamelMemoryLeak.parseSize("10B")).isEqualTo(10);
+        assertThat(CamelMemoryLeak.parseSize("10KB")).isEqualTo(10 * 1024);
+        assertThat(CamelMemoryLeak.parseSize("10k")).isEqualTo(10 * 1024);
+        assertThat(CamelMemoryLeak.parseSize("1.5MB")).isEqualTo(1536 * 1024);
+        assertThat(CamelMemoryLeak.parseSize("2G")).isEqualTo(2L * 1024 * 1024 
* 1024);
+    }
+
+    @Test
+    void notASizeIsAUsageError() {
+        assertThatThrownBy(() -> CamelMemoryLeak.parseSize("lots"))
+                .isInstanceOf(CliException.class)
+                .hasMessage("--min-size must be a size such as 1024, 10KB or 
1MB, was: lots")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
new file mode 100644
index 000000000000..e4bc88d58fc2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.util.json.JsonArray;
+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.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel cmd send: the message it sends, and the exit code of what the 
integration answers.
+ */
+@ExtendWith(MockitoExtension.class)
+class CamelSendActionResultTest extends ActionCommandTestSupport {
+
+    @TempDir
+    Path dir;
+
+    @Test
+    void sendsTheMessage() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+
+        int exit = callWithResponse(command("orders", 
"--endpoint=direct:start", "--body=hello", "--header=id=1",
+                "--timeout=100"), answer("success"));
+
+        assertThat(exit).isEqualTo(CliExitCode.OK);
+        JsonObject action = readActionFile(TEST_PID);
+        assertThat(action.getString("action")).isEqualTo("send");
+        assertThat(action.getString("endpoint")).isEqualTo("direct:start");
+        assertThat(action.getString("body")).isEqualTo("hello");
+        assertThat(action.getString("exchangePattern")).isEqualTo("InOnly");
+        // the timeout is at least 5 seconds
+        assertThat(action.getLong("pollTimeout")).isEqualTo(5000L);
+        JsonArray headers = action.getCollection("headers");
+        JsonObject header = headers.getMap(0);
+        assertThat(header.getString("key")).isEqualTo("id");
+        assertThat(header.getString("value")).isEqualTo("1");
+    }
+
+    @Test
+    void failedAndTimedOutExchanges() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+
+        // the reply message and an exception without a message
+        assertThat(callWithResponse(command("orders", 
"--endpoint=direct:start", "--reply"), answer("error")))
+                .isEqualTo(CliExitCode.FAILURE);
+        assertThat(printer.getStdout()).contains("Failed 
(exception)").contains("NullPointerException");
+        assertThat(callWithResponse(command("orders", 
"--endpoint=direct:start"), answer("timeout")))
+                .isEqualTo(CliExitCode.TIMEOUT);
+    }
+
+    @Test
+    void replyGoesToTheReplyFile() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        Path reply = dir.resolve("reply.json");
+
+        int exit = callWithResponse(command("orders", 
"--endpoint=direct:start", "--reply-file=" + reply),
+                answer("success"));
+
+        assertThat(exit).isEqualTo(CliExitCode.OK);
+        
assertThat(readActionFile(TEST_PID).getString("exchangePattern")).isEqualTo("InOut");
+        JsonObject written = Jsoner.deserialize(Files.readString(reply), 
(JsonObject) null);
+        assertThat(written.getString("exchangeId")).isEqualTo("ex-1");
+    }
+
+    @Test
+    void bodyFromAFileIsSentWithItsAbsolutePath() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        Path body = Files.writeString(dir.resolve("order.json"), "{}");
+
+        callWithResponse(command("orders", "--endpoint=direct:start", 
"--body=file:" + body), answer("success"));
+
+        
assertThat(readActionFile(TEST_PID).getString("body")).isEqualTo("file:" + 
body.toAbsolutePath());
+    }
+
+    @Test
+    void headerWithoutValueIsAUsageError() {
+        assertThatThrownBy(() -> command("orders", "--endpoint=direct:start", 
"--header=id").doCall())
+                .isInstanceOf(CliException.class)
+                .hasMessage("Header must be in key=value format, was: id")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+    }
+
+    private CamelSendAction command(String... args) {
+        CamelSendAction command = new CamelSendAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, args);
+        // the status line on the printer, not the console
+        command.loggingColor = false;
+        return command;
+    }
+
+    // the answer of the send dev console of the integration
+    private static JsonObject answer(String status) {
+        JsonObject answer = new JsonObject();
+        answer.put("timestamp", 1_700_000_000_000L);
+        answer.put("status", status);
+        answer.put("elapsed", 5L);
+        answer.put("endpoint", "direct://start");
+        answer.put("exchangeId", "ex-1");
+        if ("error".equals(status)) {
+            // an exception without a message, such as a NullPointerException
+            answer.put("exception", new JsonObject(Map.of("type", 
"java.lang.NullPointerException")));
+        }
+        JsonObject body = new JsonObject();
+        body.put("type", "java.lang.String");
+        body.put("value", "reply");
+        JsonObject message = new JsonObject();
+        message.put("exchangeType", "DefaultExchange");
+        message.put("messageType", "DefaultMessage");
+        message.put("body", body);
+        answer.put("message", message);
+        return answer;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceActionRoutesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceActionRoutesTest.java
new file mode 100644
index 000000000000..8387a56f6076
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceActionRoutesTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.json.JsonArray;
+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.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * camel get source: the source of the routes, as the integration answers it.
+ */
+@ExtendWith(MockitoExtension.class)
+class CamelSourceActionRoutesTest extends ActionCommandTestSupport {
+
+    @Test
+    void routesWithoutSourceLocation() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        // such as routes a RouteBuilder adds without a source location
+        JsonObject answer = new JsonObject(
+                Map.of("routes", new 
JsonArray(List.of(route("file:/work/orders.yaml"), route(null), route(null)))));
+
+        assertThat(callWithResponse(command("orders", "--json"), 
answer)).isZero();
+
+        JsonArray rows = Jsoner.deserialize(printer.getStdout(), (JsonArray) 
null);
+        // the routes without a location are listed once, and first
+        assertThat(rows).hasSize(2);
+        JsonObject first = rows.getMap(0);
+        assertThat(first.getString("location")).isNull();
+        JsonObject second = rows.getMap(1);
+        assertThat(second.getString("location")).isEqualTo("orders.yaml");
+    }
+
+    @Test
+    void answerWithoutRoutes() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+
+        assertThat(callWithResponse(command("orders", "--json"), new 
JsonObject())).isZero();
+
+        assertThat(printer.getStdout()).isEqualTo("[]");
+    }
+
+    private CamelSourceAction command(String... args) {
+        CamelSourceAction command = new CamelSourceAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
+
+    private static JsonObject route(String source) {
+        JsonObject line = new JsonObject();
+        line.put("line", 1);
+        line.put("code", "- route:");
+        JsonObject route = new JsonObject();
+        if (source != null) {
+            route.put("source", source);
+        }
+        route.put("code", new JsonArray(List.of(line)));
+        return route;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryActionTest.java
new file mode 100644
index 000000000000..8ec9b3ec274e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSqlQueryActionTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.action;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+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.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel cmd sql: the query it sends, and the rows or the update count the 
integration answers.
+ */
+@ExtendWith(MockitoExtension.class)
+class CamelSqlQueryActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void rowsAsATable() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        JsonObject answer = new JsonObject();
+        answer.put("columns", new JsonArray(
+                List.of(new JsonObject(Map.of("name", "ID")),
+                        new JsonObject(Map.of("name", "ITEM")))));
+        JsonObject row = new JsonObject();
+        row.put("ID", 1);
+        row.put("ITEM", "camel");
+        JsonObject empty = new JsonObject();
+        empty.put("ID", 2);
+        answer.put("rows", new JsonArray(List.of(row, empty)));
+        answer.put("rowCount", 2);
+        answer.put("elapsed", 3);
+
+        assertThat(callWithResponse(command("orders", "--query=select * from 
orders", "--max-rows=10"), answer)).isZero();
+
+        JsonObject action = readActionFile(TEST_PID);
+        assertThat(action.getString("action")).isEqualTo("sql-query");
+        assertThat(action.getString("sql")).isEqualTo("select * from orders");
+        assertThat(action.getInteger("maxRows")).isEqualTo(10);
+        assertThat(printer.getLines()).containsExactly(
+                "ID | ITEM",
+                "---+------",
+                "1  | camel",
+                "2  | null",
+                "",
+                "2 row(s) in 3ms");
+    }
+
+    @Test
+    void updateCount() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        JsonObject answer = new JsonObject(Map.of("updateCount", 4, "elapsed", 
2));
+
+        assertThat(callWithResponse(command("orders", "--query=delete from 
orders"), answer)).isZero();
+
+        assertThat(printer.getLines()).containsExactly("Update count: 4", 
"Elapsed: 2ms");
+    }
+
+    @Test
+    void failedQueryIsAnError() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        JsonObject answer = new JsonObject(Map.of("status", "error", "error", 
"Table ORDERS not found"));
+
+        assertThatThrownBy(() -> callWithResponse(command("orders", 
"--query=select * from orders"), answer))
+                .isInstanceOf(CliException.class)
+                .hasMessageContaining("Table ORDERS not found")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.FAILURE);
+    }
+
+    private CamelSqlQueryAction command(String... args) {
+        CamelSqlQueryAction command = new CamelSqlQueryAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceActionTest.java
new file mode 100644
index 000000000000..f05201da6a85
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceActionTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+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.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * camel trace: the status of tracing in the integrations, and starting, 
stopping and clearing it.
+ */
+@ExtendWith(MockitoExtension.class)
+class CamelTraceActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void statusAsJson() throws Exception {
+        writeStatusFile(TEST_PID, status(true, false));
+
+        assertThat(callWithSingleProcess(command("--json"))).isZero();
+
+        JsonArray rows = Jsoner.deserialize(printer.getStdout(), (JsonArray) 
null);
+        JsonObject row = rows.getMap(0);
+        assertThat(row.getString("name")).isEqualTo("orders");
+        assertThat(row.getString("status")).isEqualTo("Started");
+        assertThat(row.getLong("total")).isEqualTo(7L);
+        assertThat(row.getLong("queue")).isEqualTo(2L);
+        assertThat(row.getString("pattern")).isEqualTo("route*");
+    }
+
+    @Test
+    void statusOfStandbyTracing() throws Exception {
+        writeStatusFile(TEST_PID, status(false, true));
+
+        assertThat(callWithSingleProcess(command())).isZero();
+
+        assertThat(printer.getStdout()).contains("Standby");
+    }
+
+    @Test
+    void statusOfStoppedTracing() throws Exception {
+        writeStatusFile(TEST_PID, status(false, false));
+
+        assertThat(callWithSingleProcess(command())).isZero();
+
+        assertThat(printer.getStdout()).contains("Stopped");
+    }
+
+    @Test
+    void startAndStopSendTheAction() throws Exception {
+        writeStatusFile(TEST_PID, status(false, false));
+
+        assertThat(callWithSingleProcess(command("--action=start"))).isZero();
+        
assertThat(readActionFile(TEST_PID).getString("action")).isEqualTo("trace");
+        
assertThat(readActionFile(TEST_PID).getString("enabled")).isEqualTo("true");
+
+        assertThat(callWithSingleProcess(command("--action=stop"))).isZero();
+        
assertThat(readActionFile(TEST_PID).getString("enabled")).isEqualTo("false");
+    }
+
+    @Test
+    void clearEmptiesTheTraceFile() throws Exception {
+        writeStatusFile(TEST_PID, status(true, false));
+        Path traceFile = CommandLineHelper.getCamelDir().resolve(TEST_PID + 
"-trace.json");
+        Files.writeString(traceFile, "{\"traces\":[{\"uid\":1}]}");
+
+        assertThat(callWithSingleProcess(command("--action=clear"))).isZero();
+
+        assertThat(Files.readString(traceFile)).isEqualTo("{}");
+    }
+
+    private CamelTraceAction command(String... args) {
+        CamelTraceAction command = new CamelTraceAction(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
+
+    private static JsonObject status(boolean enabled, boolean standby) {
+        JsonObject trace = new JsonObject();
+        trace.put("enabled", enabled);
+        trace.put("standby", standby);
+        trace.put("counter", 7);
+        trace.put("queueSize", 2);
+        trace.put("traceFilter", "*");
+        trace.put("tracePattern", "route*");
+        JsonObject context = new JsonObject();
+        context.put("name", "orders");
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("trace", trace);
+        return root;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionResultTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionResultTest.java
new file mode 100644
index 000000000000..9ea409f748d4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionResultTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.action;
+
+import java.util.Map;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+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.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel eval expression in a running integration: what it sends, and the 
result or the error it prints.
+ */
+@ExtendWith(MockitoExtension.class)
+class EvalExpressionResultTest extends ActionCommandTestSupport {
+
+    @Test
+    void resultOfTheExpression() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+
+        int exit = callWithResponse(command("orders", 
"--template=${body.length()}", "--body=hello", "--header=id=1",
+                "--variable=total=2"), new JsonObject(Map.of("status", 
"success", "result", "5")));
+
+        assertThat(exit).isEqualTo(CliExitCode.OK);
+        assertThat(printer.getStdout()).isEqualTo("5");
+        JsonObject action = readActionFile(TEST_PID);
+        assertThat(action.getString("action")).isEqualTo("eval");
+        assertThat(action.getString("language")).isEqualTo("simple");
+        assertThat(action.getString("template")).isEqualTo("${body.length()}");
+        assertThat(action.getString("body")).isEqualTo("hello");
+        JsonArray headers = action.getCollection("headers");
+        JsonObject header = headers.getMap(0);
+        assertThat(header.getString("key")).isEqualTo("id");
+        assertThat(header.getString("value")).isEqualTo("1");
+        JsonArray variables = action.getCollection("variables");
+        JsonObject variable = variables.getMap(0);
+        assertThat(variable.getString("key")).isEqualTo("total");
+    }
+
+    @Test
+    void failedEvaluationIsAnError() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+        JsonObject exception = new JsonObject(Map.of("message", "Unknown 
function: foo", "stackTrace", "at Simple"));
+        JsonObject answer = new JsonObject(Map.of("status", "failed", 
"exception", exception));
+
+        assertThatThrownBy(() -> callWithResponse(command("orders", 
"--template=${foo}"), answer))
+                .isInstanceOf(CliException.class)
+                .hasMessage("Unknown function: foo")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.FAILURE);
+        // the stack trace on stderr
+        assertThat(printer.getStderr()).contains("at Simple");
+    }
+
+    @Test
+    void headerWithoutValueIsAUsageError() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+
+        assertThatThrownBy(() -> callWithResponse(command("orders", 
"--template=${body}", "--header=id"), null))
+                .isInstanceOf(CliException.class)
+                .hasMessage("Header must be in key=value format, was: id")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+    }
+
+    private EvalExpressionCommand command(String... args) {
+        EvalExpressionCommand command = new EvalExpressionCommand(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSetTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSetTest.java
index 946098e91d6f..4964013269d6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSetTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/ConfigSetTest.java
@@ -19,6 +19,8 @@ package org.apache.camel.dsl.jbang.core.commands.config;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.UserConfigHelper;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -72,4 +74,29 @@ class ConfigSetTest extends BaseConfigTestSupport {
         }, true);
     }
 
+    @Test
+    public void shouldRejectConfigWithoutKey() throws Exception {
+        UserConfigHelper.createUserConfig("");
+
+        ConfigSet command = new ConfigSet(new 
CamelJBangMain().withPrinter(printer));
+        command.configuration = "=bar";
+
+        CliException e = Assertions.assertThrows(CliException.class, 
command::doCall);
+        Assertions.assertEquals(CliExitCode.USAGE, e.getExitCode());
+        Assertions.assertEquals("Configuration parameter not in key=value 
format: =bar", e.getMessage());
+        CommandLineHelper.loadProperties(properties -> 
Assertions.assertTrue(properties.isEmpty()));
+    }
+
+    @Test
+    public void shouldRejectConfigWithoutValue() throws Exception {
+        ConfigSet command = new ConfigSet(new 
CamelJBangMain().withPrinter(printer));
+        command.configuration = "foo";
+        command.global = false;
+
+        CliException e = Assertions.assertThrows(CliException.class, 
command::doCall);
+        Assertions.assertEquals(CliExitCode.USAGE, e.getExitCode());
+        // no local configuration file is created
+        Assertions.assertFalse(CommandLineHelper.hasLocalUserConfig());
+    }
+
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRunSelectionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRunSelectionTest.java
new file mode 100644
index 000000000000..ff71215e7e43
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraRunSelectionTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.infra;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel infra run: a service or implementation that is not in the catalog of 
the test infra is not found.
+ */
+class InfraRunSelectionTest extends CamelCommandBaseTestSupport {
+
+    @Test
+    void unknownServiceIsNotFound() {
+        assertThatThrownBy(() -> command("nope").doCall())
+                .isInstanceOf(CliException.class)
+                .hasMessage("service nope not found, use the list command for 
the available services")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+    }
+
+    @Test
+    void unknownImplementationIsNotFound() {
+        assertThatThrownBy(() -> command("ftp", "nope").doCall())
+                .isInstanceOf(CliException.class)
+                .hasMessage("service ftp with implementation nope not found, 
use the list command for the available services")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+    }
+
+    @Test
+    void malformedPropertyIsAUsageError() {
+        assertThatThrownBy(() -> command("ftp", "--property=novalue").doCall())
+                .isInstanceOf(CliException.class)
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.USAGE);
+    }
+
+    private InfraRun command(String... args) {
+        InfraRun command = new InfraRun(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(command, args);
+        return command;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraStopTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraStopTest.java
new file mode 100644
index 000000000000..96942372ac4c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraStopTest.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.core.commands.infra;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel infra stop: a service is stopped by removing its pid file, which the 
infra run of the service watches.
+ */
+class InfraStopTest extends InfraCommandTestSupport {
+
+    @Test
+    void stopsTheMatchingService() throws Exception {
+        writePidFile("kafka", 1234, "{}");
+        writePidFile("ftp", 5678, "{}");
+
+        assertThat(command("kafka").doCall()).isZero();
+
+        assertThat(pidFile("kafka", 1234)).doesNotExist();
+        assertThat(pidFile("ftp", 5678)).exists();
+        assertThat(printer.getStdout()).isEqualTo("Shutting down external 
services (PID: 1234)");
+    }
+
+    @Test
+    void stopsAllServices() throws Exception {
+        writePidFile("kafka", 1234, "{}");
+        writePidFile("ftp", 5678, "{}");
+
+        assertThat(command("*").doCall()).isZero();
+
+        assertThat(pidFile("kafka", 1234)).doesNotExist();
+        assertThat(pidFile("ftp", 5678)).doesNotExist();
+    }
+
+    @Test
+    void stopsByPid() throws Exception {
+        writePidFile("kafka", 1234, "{}");
+
+        assertThat(command("1234").doCall()).isZero();
+
+        assertThat(pidFile("kafka", 1234)).doesNotExist();
+    }
+
+    @Test
+    void stopsAsJson() throws Exception {
+        writePidFile("kafka", 1234, "{}");
+
+        InfraStop command = command("kafka");
+        command.jsonOutput = true;
+        assertThat(command.doCall()).isZero();
+
+        JsonArray rows = Jsoner.deserialize(printer.getStdout(), (JsonArray) 
null);
+        JsonObject row = rows.getMap(0);
+        assertThat(row.getString("alias")).isEqualTo("kafka");
+        assertThat(row.getString("status")).isEqualTo("stopping");
+    }
+
+    @Test
+    void serviceThatIsNotRunningIsNotFound() throws Exception {
+        writePidFile("kafka", 1234, "{}");
+
+        assertThatThrownBy(() -> command("ftp").doCall())
+                .isInstanceOf(CliException.class)
+                .hasMessage("No running infra service matches: ftp")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+        assertThat(pidFile("kafka", 1234)).exists();
+    }
+
+    @Test
+    void serviceWhoseProcessIsGoneIsNotFound() throws Exception {
+        writePidFile("kafka", 1234, "{}");
+        writeLogFile("kafka", 1234, "started");
+
+        InfraStop command = command("kafka");
+        command.aliveCheck = pid -> false;
+
+        assertThatThrownBy(command::doCall)
+                .isInstanceOf(CliException.class)
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+        // the files it left behind are removed
+        assertThat(pidFile("kafka", 1234)).doesNotExist();
+        assertThat(logFile("kafka", 1234)).doesNotExist();
+    }
+
+    @Test
+    void nothingToStop() throws Exception {
+        assertThat(command("*").doCall()).isZero();
+
+        assertThat(printer.getStdout()).isEmpty();
+    }
+
+    private InfraStop command(String name) {
+        InfraStop command = new InfraStop(new 
CamelJBangMain().withPrinter(printer));
+        command.name = name;
+        // the pid files have made-up pids
+        command.aliveCheck = pid -> true;
+        return command;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/JolokiaTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/JolokiaTest.java
new file mode 100644
index 000000000000..d2a08e76a554
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/JolokiaTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.process;
+
+import java.util.List;
+import java.util.stream.LongStream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mockStatic;
+
+/**
+ * camel jolokia attaches to exactly one running integration.
+ */
+@ExtendWith(MockitoExtension.class)
+class JolokiaTest extends ProcessCommandTestSupport {
+
+    @Test
+    void nameThatMatchesNoIntegrationIsNotFound() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("orders", 5));
+
+        assertThatThrownBy(() -> call("billing", TEST_PID))
+                .isInstanceOf(CliException.class)
+                .hasMessage("No running Camel integration matches: billing")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+    }
+
+    @Test
+    void nameThatMatchesSeveralIntegrationsIsNotFound() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("orders", 5));
+        writeStatusFile(TEST_PID + 1, buildContextStatus("orders", 5));
+
+        assertThatThrownBy(() -> call("orders", TEST_PID, TEST_PID + 1))
+                .isInstanceOf(CliException.class)
+                .hasMessageStartingWith("Name or pid orders matches 2 running 
Camel integrations")
+                .extracting(e -> ((CliException) 
e).getExitCode()).isEqualTo(CliExitCode.NOT_FOUND);
+    }
+
+    private int call(String name, long... pids) throws Exception {
+        Jolokia command = new Jolokia(new 
CamelJBangMain().withPrinter(printer));
+        command.name = name;
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            List<ProcessHandle> handles = 
LongStream.of(pids).mapToObj(ProcessCommandTestSupport::mockProcessHandle).toList();
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
handles.stream());
+            return command.doCall();
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListDataSourceTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListDataSourceTest.java
new file mode 100644
index 000000000000..a287cce49683
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListDataSourceTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.process;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.json.JsonArray;
+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.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mockStatic;
+
+/**
+ * camel get datasource: the connection pools of the integrations.
+ */
+@ExtendWith(MockitoExtension.class)
+class ListDataSourceTest extends ProcessCommandTestSupport {
+
+    @Test
+    void dataSourcesAsJson() throws Exception {
+        JsonObject root = buildContextStatus("orders", 5);
+        root.put("dataSources", dataSources(pool("default", 2, 3)));
+        writeStatusFile(TEST_PID, root);
+
+        ListDataSource command = new ListDataSource(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+        command.sort = "pid";
+
+        assertThat(call(command)).isZero();
+
+        JsonArray rows = Jsoner.deserialize(printer.getStdout(), (JsonArray) 
null);
+        JsonObject row = rows.getMap(0);
+        assertThat(row.getLong("pid")).isEqualTo(TEST_PID);
+        assertThat(row.getString("name")).isEqualTo("orders");
+        assertThat(row.getString("dsName")).isEqualTo("default");
+        assertThat(row.getString("poolType")).isEqualTo("HikariCP");
+        assertThat(row.getInteger("active")).isEqualTo(2);
+        assertThat(row.getInteger("idle")).isEqualTo(3);
+        assertThat(row.getInteger("total")).isEqualTo(5);
+        assertThat(row.getInteger("maxPoolSize")).isEqualTo(10);
+    }
+
+    @Test
+    void tableOfTheDataSources() throws Exception {
+        JsonObject root = buildContextStatus("orders", 5);
+        root.put("dataSources", dataSources(pool("default", 2, 3), 
pool("audit", 0, 1)));
+        writeStatusFile(TEST_PID, root);
+
+        ListDataSource command = new ListDataSource(new 
CamelJBangMain().withPrinter(printer));
+        command.sort = "pid";
+
+        assertThat(call(command)).isZero();
+
+        
assertThat(printer.getStdout()).contains("default-pool").contains("audit-pool");
+    }
+
+    @Test
+    void integrationWithoutDataSourcesListsNothing() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("orders", 5));
+
+        ListDataSource command = new ListDataSource(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+        command.sort = "pid";
+
+        assertThat(call(command)).isZero();
+
+        assertThat(printer.getStdout()).isEqualTo("[]");
+    }
+
+    private static JsonObject dataSources(JsonObject... pools) {
+        JsonObject answer = new JsonObject();
+        answer.put("dataSources", new JsonArray(List.of(pools)));
+        return answer;
+    }
+
+    private static JsonObject pool(String name, int active, int idle) {
+        JsonObject pool = new JsonObject();
+        pool.put("name", name);
+        pool.put("type", "org.postgresql.ds.PGSimpleDataSource");
+        pool.put("poolType", "HikariCP");
+        pool.put("poolName", name + "-pool");
+        pool.put("active", active);
+        pool.put("idle", idle);
+        pool.put("total", active + idle);
+        pool.put("maxPoolSize", 10);
+        pool.put("waiting", 0);
+        return pool;
+    }
+
+    private int call(ListDataSource command) throws Exception {
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+            return command.doCall();
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListSqlTraceTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListSqlTraceTest.java
new file mode 100644
index 000000000000..3a196953930d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListSqlTraceTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.process;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.json.JsonArray;
+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.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mockStatic;
+
+/**
+ * camel get sql-trace: the SQL statements the integrations ran, when SQL 
tracing is on.
+ */
+@ExtendWith(MockitoExtension.class)
+class ListSqlTraceTest extends ProcessCommandTestSupport {
+
+    @Test
+    void statementsAsJson() throws Exception {
+        JsonObject root = buildContextStatus("orders", 5);
+        root.put("sqlTrace", statements(statement("SELECT * FROM orders", 12, 
false),
+                statement("UPDATE orders SET done = true", 3, true)));
+        writeStatusFile(TEST_PID, root);
+
+        ListSqlTrace command = new ListSqlTrace(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+        command.sort = "pid";
+
+        assertThat(call(command)).isZero();
+
+        JsonArray rows = Jsoner.deserialize(printer.getStdout(), (JsonArray) 
null);
+        assertThat(rows).hasSize(2);
+        JsonObject select = rows.getMap(0);
+        assertThat(select.getLong("pid")).isEqualTo(TEST_PID);
+        assertThat(select.getString("query")).isEqualTo("SELECT * FROM 
orders");
+        assertThat(select.getString("routeId")).isEqualTo("route1");
+        assertThat(select.getLong("duration")).isEqualTo(12L);
+        assertThat(select.getInteger("rowCount")).isEqualTo(4);
+        assertThat(select.getBoolean("failed")).isFalse();
+        JsonObject update = rows.getMap(1);
+        assertThat(update.getBoolean("failed")).isTrue();
+    }
+
+    @Test
+    void tableOfTheStatements() throws Exception {
+        JsonObject root = buildContextStatus("orders", 5);
+        root.put("sqlTrace", statements(statement("SELECT * FROM orders", 12, 
false)));
+        writeStatusFile(TEST_PID, root);
+
+        ListSqlTrace command = new ListSqlTrace(new 
CamelJBangMain().withPrinter(printer));
+        command.sort = "pid";
+
+        assertThat(call(command)).isZero();
+
+        // a long statement is cut to fit the table
+        assertThat(printer.getStdout()).contains("SELECT * FROM 
ord").contains("route1").contains("OK");
+    }
+
+    @Test
+    void withoutSqlTracingListsNothing() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("orders", 5));
+
+        ListSqlTrace command = new ListSqlTrace(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+        command.sort = "pid";
+
+        assertThat(call(command)).isZero();
+
+        assertThat(printer.getStdout()).isEqualTo("[]");
+    }
+
+    private static JsonObject statements(JsonObject... statements) {
+        JsonObject answer = new JsonObject();
+        answer.put("statements", new JsonArray(List.of(statements)));
+        return answer;
+    }
+
+    private static JsonObject statement(String query, long duration, boolean 
failed) {
+        JsonObject statement = new JsonObject();
+        statement.put("timestamp", 1000L);
+        statement.put("exchangeId", "ex-1");
+        statement.put("routeId", "route1");
+        statement.put("query", query);
+        statement.put("category", "jdbc");
+        statement.put("endpoint", "sql://orders");
+        statement.put("duration", duration);
+        statement.put("rowCount", 4);
+        statement.put("updateCount", 0);
+        statement.put("failed", failed);
+        return statement;
+    }
+
+    private int call(ListSqlTrace command) throws Exception {
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph));
+            return command.doCall();
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
index dec47a966cb2..80740006e994 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ActionClientTest.java
@@ -179,6 +179,15 @@ class ActionClientTest {
         
assertThat(ActionClient.result(json("{\"value\":1}")).status()).isEqualTo(Status.OK);
     }
 
+    @Test
+    void errorOfAnExceptionWithoutMessage() {
+        // camel cmd send --reply: the exchange failed with a 
NullPointerException, and the answer has the reply message
+        JsonObject answer = 
json("{\"status\":\"error\",\"exception\":{\"type\":\"java.lang.NullPointerException\"},"
+                                 + 
"\"message\":{\"body\":{\"type\":\"java.lang.String\",\"value\":\"hello\"}}}");
+
+        
assertThat(ActionClient.result(answer).error()).isEqualTo("java.lang.NullPointerException");
+    }
+
     @Test
     void versions() {
         assertThat(ActionClient.isAtLeast("4.24.0-SNAPSHOT", 4, 24)).isTrue();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GitHubHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GitHubHelperTest.java
new file mode 100644
index 000000000000..be9ce314132c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/GitHubHelperTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.common;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Which files of a GitHub repository or a gist camel run takes as routes.
+ */
+class GitHubHelperTest {
+
+    @ParameterizedTest
+    @CsvSource({
+            "java, true",
+            "xml, true",
+            "camel.xml, true",
+            "yaml, true",
+            "camel.yaml, true",
+            "YAML, true",
+            "properties, false",
+            "md, false",
+            "kamelet.yaml, false" })
+    void routeFiles(String ext, boolean route) {
+        assertThat(GitHubHelper.isRouteExtension(ext)).isEqualTo(route);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessHelperTest.java
new file mode 100644
index 000000000000..0c903306ca6c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ProcessHelperTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.common;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * The name the CLI shows for a running integration (camel ps, camel log and 
the other commands).
+ */
+class ProcessHelperTest {
+
+    private static final String MAVEN
+            = "/usr/bin/java -classpath plexus-classworlds-2.8.0.jar 
org.codehaus.plexus.classworlds.launcher.Launcher";
+
+    @Test
+    void mainClassOfTheIntegration() {
+        JsonObject root = root("camel-1");
+        root.put("runtime", new JsonObject(Map.of("mainClass", 
"com.acme.OrdersApplication")));
+
+        assertThat(ProcessHelper.extractName(root, process("java -jar 
orders.jar", null)))
+                .isEqualTo("com.acme.OrdersApplication");
+    }
+
+    @Test
+    void routeFilesOfCamelRun() {
+        ProcessHandle ph = process(
+                "java -cp camel-launcher.jar main.CamelJBang run 
src/hello.camel.yaml README.md Foo.java application.properties",
+                null);
+
+        // only the route files, without their directory
+        assertThat(ProcessHelper.extractName(root("CamelJBang"), 
ph)).isEqualTo("hello.camel.yaml Foo.java");
+    }
+
+    @Test
+    void mainClassOnTheCommandLine() {
+        assertThat(ProcessHelper.extractName(root("camel-1"), process("java 
-cp lib/* com.acme.Main", null)))
+                .isEqualTo("com.acme.Main");
+    }
+
+    @Test
+    void nameOfTheCamelContextWithoutACommandLine() {
+        assertThat(ProcessHelper.extractName(root("orders"), process(null, 
null))).isEqualTo("orders");
+        assertThat(ProcessHelper.extractName(root("orders"), 
null)).isEqualTo("orders");
+        assertThat(ProcessHelper.extractName(null, null)).isEmpty();
+    }
+
+    @Test
+    void springBootRunIsTheProcessMavenStarted() {
+        ProcessHandle maven = process(MAVEN + " spring-boot:run", null);
+        ProcessHandle app = process("java -cp camel-spring-boot-4.24.0.jar 
com.acme.Application", maven);
+        when(maven.children()).thenAnswer(inv -> Stream.of(app));
+
+        // Camel runs in the process Maven started, so Maven itself has no 
name to be listed with
+        assertThat(ProcessHelper.extractName(null, maven)).isEmpty();
+        assertThat(ProcessHelper.extractName(null, 
app)).isEqualTo("com.acme.Application");
+    }
+
+    @Test
+    void quarkusDevIsNamedAfterTheMavenGoal() {
+        ProcessHandle maven = process(MAVEN + " quarkus:dev", null);
+        ProcessHandle app = process("java -cp camel-quarkus-core-3.30.0.jar 
-jar quarkus-run.jar", maven);
+
+        assertThat(ProcessHelper.extractName(null, 
app)).isEqualTo("quarkus:dev");
+    }
+
+    private static JsonObject root(String contextName) {
+        JsonObject root = new JsonObject();
+        root.put("context", new JsonObject(Map.of("name", contextName)));
+        return root;
+    }
+
+    private static ProcessHandle process(String commandLine, ProcessHandle 
parent) {
+        ProcessHandle.Info info = mock(ProcessHandle.Info.class);
+        when(info.commandLine()).thenReturn(Optional.ofNullable(commandLine));
+        ProcessHandle ph = mock(ProcessHandle.class);
+        when(ph.info()).thenReturn(info);
+        when(ph.parent()).thenReturn(Optional.ofNullable(parent));
+        when(ph.children()).thenAnswer(inv -> Stream.empty());
+        return ph;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/VersionHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/VersionHelperTest.java
new file mode 100644
index 000000000000..9b23ff183ed6
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/VersionHelperTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.common;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Versions compare part by part as numbers: the export gates of the Quarkus 
platform (whose versions have four parts),
+ * the order of camel version list, the self update of the launcher and the 
security advisories use it.
+ */
+class VersionHelperTest {
+
+    @ParameterizedTest
+    @CsvSource({
+            "4.24.0, 4.24.0, 0",
+            "4.24, 4.24.0, 0",
+            "4.9.0, 4.10.0, -1",
+            "4.10.0, 4.9.0, 1",
+            "3.27.3.1, 3.27.4, -1",
+            "3.33.1.1, 3.33.1, 1",
+            "3.31.0.1, 3.31.0, 1",
+            "4.99.0, 4.100.0, -1",
+            "4.24.0, 4.24.0-SNAPSHOT, 1",
+            "4.24.10-SNAPSHOT, 4.24.10, -1",
+            "4.24.0-SNAPSHOT, 4.23.0, 1",
+            "4.24.0-SNAPSHOT, 4.24.0-SNAPSHOT, 0",
+            "4.24.0-RC1, 4.24.0-RC2, -1",
+            "4.24.0-RC1, 4.24.0, -1",
+            "4.24.0-M2, 4.24.0-RC1, -1",
+            // a build of a release comes after it
+            "4.24.0.build-2, 4.24.0, 1",
+            "4.24.0.build-2, 4.24.0-SNAPSHOT, 1",
+            "4.24.0-beta-1, 4.24.0, -1",
+            "'4,10', '4,9', 1" })
+    void compare(String source, String target, int expected) {
+        assertThat(Integer.signum(VersionHelper.compare(source, 
target))).isEqualTo(expected);
+    }
+
+    @Test
+    void missingVersionIsTheSame() {
+        assertThat(VersionHelper.compare(null, "4.24.0")).isZero();
+        assertThat(VersionHelper.compare("4.24.0", " ")).isZero();
+    }
+
+    @Test
+    void ranges() {
+        assertThat(VersionHelper.isGE("3.31.0.1", "3.31.0")).isTrue();
+        assertThat(VersionHelper.isLE("3.27.3.1", "3.27.4")).isTrue();
+        // a snapshot comes before its release, as in Maven
+        assertThat(VersionHelper.isGE("4.24.0-SNAPSHOT", "4.24.0")).isFalse();
+        assertThat(VersionHelper.isGE("4.24.0-SNAPSHOT", "4.23.0")).isTrue();
+        assertThat(VersionHelper.isBetween("4.18.2", "4.18.0", 
"4.19.0")).isTrue();
+        assertThat(VersionHelper.isBetween("4.19.0", "4.18.0", 
"4.19.0")).isFalse();
+    }
+
+    @Test
+    void majorMinor() {
+        
assertThat(VersionHelper.getMajorMinorVersion("4.24.1")).isEqualTo("4.24");
+    }
+}

Reply via email to