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 786282b0529c CAMEL-23688: Add unit tests for camel-jbang-core process/ 
commands
786282b0529c is described below

commit 786282b0529c8d6127aec5e5424cc99b2e6eda5c
Author: Adriano Machado <[email protected]>
AuthorDate: Tue Jun 9 07:32:03 2026 -0400

    CAMEL-23688: Add unit tests for camel-jbang-core process/ commands
    
    Add 13 new test classes covering the process/ package commands that
    previously had no unit test coverage: CamelContextTop, CamelCount,
    CamelProcessorStatus, CamelProcessorTop, CamelRouteGroupStatus,
    CamelRouteGroupTop, CamelRouteTop, Dirty, ListGroovy, ListKafka,
    ListPlatformHttp, RestartProcess, and StopProcess. Also improves
    ListProcessTest.testSortByName to verify sort ordering is
    insertion-order independent.
    
    Closes #23869
---
 .../core/commands/process/CamelContextTopTest.java | 186 ++++++++++++++++++++
 .../core/commands/process/CamelCountTest.java      | 176 +++++++++++++++++++
 .../commands/process/CamelProcessorStatusTest.java | 167 ++++++++++++++++++
 .../commands/process/CamelProcessorTopTest.java    | 120 +++++++++++++
 .../process/CamelRouteGroupStatusTest.java         | 170 +++++++++++++++++++
 .../commands/process/CamelRouteGroupTopTest.java   | 126 ++++++++++++++
 .../core/commands/process/CamelRouteTopTest.java   | 137 +++++++++++++++
 .../dsl/jbang/core/commands/process/DirtyTest.java | 109 ++++++++++++
 .../core/commands/process/ListGroovyTest.java      | 136 +++++++++++++++
 .../jbang/core/commands/process/ListKafkaTest.java | 145 ++++++++++++++++
 .../commands/process/ListPlatformHttpTest.java     | 188 +++++++++++++++++++++
 .../core/commands/process/ListProcessTest.java     |  25 ++-
 .../core/commands/process/RestartProcessTest.java  |  75 ++++++++
 .../core/commands/process/StopProcessTest.java     | 102 +++++++++++
 14 files changed, 1854 insertions(+), 8 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextTopTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextTopTest.java
new file mode 100644
index 000000000000..d87e0c163ad1
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextTopTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.apache.camel.util.json.JsonObject;
+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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class CamelContextTopTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testDisplaysMemoryAndThreadFields() throws Exception {
+        JsonObject status = buildContextStatus("topApp", 5);
+        addMemoryAndThreads(status, 256_000_000L, 8, 10);
+        writeStatusFile(TEST_PID, status);
+
+        CamelContextTop command = new CamelContextTop(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("topApp"), "Should display context 
name");
+            assertTrue(output.contains("HEAP"), "Should display HEAP column 
header");
+            assertTrue(output.contains("THREADS"), "Should display THREADS 
column header");
+        }
+    }
+
+    @Test
+    void testDisplaysThroughputAndLoad() throws Exception {
+        JsonObject status = buildContextStatus("loadApp", 5);
+        JsonObject ctx = (JsonObject) status.get("context");
+        JsonObject stats = (JsonObject) ctx.get("statistics");
+        stats.put("exchangesThroughput", "12.5");
+        stats.put("load01", "0.10");
+        stats.put("load05", "0.20");
+        stats.put("load15", "0.05");
+        writeStatusFile(TEST_PID, status);
+
+        CamelContextTop command = new CamelContextTop(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("loadApp"), "Should show context name");
+            // non-zero load values should appear as load1/load5/load15
+            assertTrue(output.contains("0.10"), "Should show load01 value");
+        }
+    }
+
+    @Test
+    void testSortByMemShowsHigherHeapFirst() throws Exception {
+        long highPid = TEST_PID;
+        long lowPid = TEST_PID + 1;
+
+        JsonObject highMem = buildContextStatus("highMem", 5);
+        addMemoryAndThreads(highMem, 500_000_000L, 8, 10);
+        writeStatusFile(highPid, highMem);
+
+        JsonObject lowMem = buildContextStatus("lowMem", 5);
+        addMemoryAndThreads(lowMem, 100_000_000L, 4, 5);
+        writeStatusFile(lowPid, lowMem);
+
+        // low process first — sort must promote high to top
+        assertHighBeforeLow(lowPid, highPid);
+        // high process first — sort must preserve the order
+        printer = new StringPrinter();
+        assertHighBeforeLow(highPid, lowPid);
+    }
+
+    private void assertHighBeforeLow(long firstPid, long secondPid) throws 
Exception {
+        CamelContextTop command = new CamelContextTop(new 
CamelJBangMain().withPrinter(printer));
+        command.sort = "mem";
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph1 = mockProcessHandle(firstPid);
+            ProcessHandle ph2 = mockProcessHandle(secondPid);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            // fresh stream per invocation so allProcesses() can be called 
multiple times
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph1, ph2));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            int highIdx = output.indexOf("highMem");
+            int lowIdx = output.indexOf("lowMem");
+            assertTrue(highIdx < lowIdx, "Higher heap should appear first 
regardless of stream order");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        CamelContextTop command = new CamelContextTop(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testJsonOutput() throws Exception {
+        JsonObject status = buildContextStatus("jsonTopApp", 5);
+        addMemoryAndThreads(status, 128_000_000L, 4, 4);
+        writeStatusFile(TEST_PID, status);
+
+        CamelContextTop command = new CamelContextTop(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.startsWith("["), "JSON output should be an 
array");
+            assertTrue(output.contains("jsonTopApp"), "JSON should contain 
context name");
+            assertTrue(output.contains("heap"), "JSON should contain heap 
field");
+        }
+    }
+
+    private static void addMemoryAndThreads(JsonObject root, long heapUsed, 
int threads, int peakThreads) {
+        JsonObject mem = new JsonObject();
+        mem.put("heapMemoryUsed", heapUsed);
+        mem.put("heapMemoryCommitted", heapUsed + 50_000_000L);
+        mem.put("heapMemoryMax", 1_000_000_000L);
+        mem.put("nonHeapMemoryUsed", 64_000_000L);
+        mem.put("nonHeapMemoryCommitted", 70_000_000L);
+        root.put("memory", mem);
+
+        JsonObject th = new JsonObject();
+        th.put("threadCount", threads);
+        th.put("peakThreadCount", peakThreads);
+        root.put("threads", th);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelCountTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelCountTest.java
new file mode 100644
index 000000000000..9d4b9f3c98f4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelCountTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.json.JsonObject;
+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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class CamelCountTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testShowsTableWithTotalsAndFailed() throws Exception {
+        JsonObject status = buildContextStatus("countApp", 5);
+        JsonObject stats = (JsonObject) ((JsonObject) 
status.get("context")).get("statistics");
+        stats.put("exchangesTotal", "42");
+        stats.put("exchangesFailed", "3");
+        writeStatusFile(TEST_PID, status);
+
+        CamelCount command = new CamelCount(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("countApp"), "Should show context 
name");
+            assertTrue(output.contains("42"), "Should show total exchanges");
+            assertTrue(output.contains("3"), "Should show failed exchanges");
+        }
+    }
+
+    @Test
+    void testTotalFlagOutputsOnlyCount() throws Exception {
+        JsonObject status = buildContextStatus("totalApp", 5);
+        JsonObject stats = (JsonObject) ((JsonObject) 
status.get("context")).get("statistics");
+        stats.put("exchangesTotal", "100");
+        stats.put("exchangesFailed", "5");
+        writeStatusFile(TEST_PID, status);
+
+        CamelCount command = new CamelCount(new 
CamelJBangMain().withPrinter(printer));
+        command.total = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("100", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testFailFlagOutputsOnlyFailedCount() throws Exception {
+        JsonObject status = buildContextStatus("failApp", 5);
+        JsonObject stats = (JsonObject) ((JsonObject) 
status.get("context")).get("statistics");
+        stats.put("exchangesTotal", "50");
+        stats.put("exchangesFailed", "7");
+        writeStatusFile(TEST_PID, status);
+
+        CamelCount command = new CamelCount(new 
CamelJBangMain().withPrinter(printer));
+        command.fail = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("7", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testTotalAndFailFlagsOutputCommaSeparated() throws Exception {
+        JsonObject status = buildContextStatus("bothApp", 5);
+        JsonObject stats = (JsonObject) ((JsonObject) 
status.get("context")).get("statistics");
+        stats.put("exchangesTotal", "20");
+        stats.put("exchangesFailed", "2");
+        writeStatusFile(TEST_PID, status);
+
+        CamelCount command = new CamelCount(new 
CamelJBangMain().withPrinter(printer));
+        command.total = true;
+        command.fail = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("20,2", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        CamelCount command = new CamelCount(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testJsonOutput() throws Exception {
+        JsonObject status = buildContextStatus("jsonCountApp", 5);
+        JsonObject stats = (JsonObject) ((JsonObject) 
status.get("context")).get("statistics");
+        stats.put("exchangesTotal", "15");
+        stats.put("exchangesFailed", "1");
+        writeStatusFile(TEST_PID, status);
+
+        CamelCount command = new CamelCount(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.startsWith("["), "JSON output should be an 
array");
+            assertTrue(output.contains("jsonCountApp"), "JSON should contain 
context name");
+            assertTrue(output.contains("total"), "JSON should contain total 
field");
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.java
new file mode 100644
index 000000000000..af78243b22fd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorStatusTest.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.process;
+
+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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class CamelProcessorStatusTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testDisplaysProcessorRow() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "25", "0", "0"));
+
+        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("myApp"), "Should show integration 
name");
+            assertTrue(output.contains("myRoute"), "Should show route ID");
+            assertTrue(output.contains("25"), "Should show mean processing 
time");
+        }
+    }
+
+    @Test
+    void testFilterMeanExcludesSlowProcessors() throws Exception {
+        // processor with mean=5 should be excluded when --filter-mean=10
+        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "5", "0", "0"));
+
+        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
+        command.mean = 10;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "Processor below mean 
threshold should be filtered out");
+        }
+    }
+
+    @Test
+    void testFilterMeanIncludesSlowEnoughProcessors() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "50", "0", "0"));
+
+        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
+        command.mean = 10;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertTrue(printer.getOutput().contains("50"), "Processor above 
mean threshold should be visible");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testJsonOutput() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithProcessor("myRoute", "log1", 
"log:output", "Started", "30", "2", "0"));
+
+        CamelProcessorStatus command = new CamelProcessorStatus(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.startsWith("["), "JSON output should be an 
array");
+            assertTrue(output.contains("myRoute"), "JSON should contain route 
ID");
+        }
+    }
+
+    private static JsonObject buildStatusWithProcessor(
+            String routeId, String processorId, String from,
+            String state, String mean, String failed, String inflight) {
+        JsonObject stats = new JsonObject();
+        stats.put("exchangesTotal", "10");
+        stats.put("exchangesFailed", failed);
+        stats.put("exchangesInflight", inflight);
+        stats.put("meanProcessingTime", mean);
+        stats.put("maxProcessingTime", mean);
+        stats.put("minProcessingTime", mean);
+
+        JsonObject route = new JsonObject();
+        route.put("routeId", routeId);
+        route.put("processorId", processorId);
+        route.put("from", from);
+        route.put("state", state);
+        route.put("statistics", stats);
+
+        JsonArray routes = new JsonArray();
+        routes.add(route);
+
+        JsonObject context = new JsonObject();
+        context.put("name", "myApp");
+        context.put("phase", 5);
+
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("routes", routes);
+        return root;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorTopTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorTopTest.java
new file mode 100644
index 000000000000..d6ec6860af70
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelProcessorTopTest.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.stream.Stream;
+
+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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class CamelProcessorTopTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testSortsByMeanDescendingSlowerProcessorFirst() throws Exception {
+        long slowPid = TEST_PID;
+        long fastPid = TEST_PID + 1;
+        // same context name so primary sort (name) ties and mean tiebreaker 
fires
+        writeStatusFile(slowPid, buildStatusWithOneProcessor("myApp", 
"slowRoute", "200"));
+        writeStatusFile(fastPid, buildStatusWithOneProcessor("myApp", 
"fastRoute", "10"));
+
+        // fast process first in stream — sort must correct the order
+        assertSlowBeforeFast(fastPid, slowPid);
+        // slow process first in stream — sort must preserve the order
+        printer = new StringPrinter();
+        assertSlowBeforeFast(slowPid, fastPid);
+    }
+
+    private void assertSlowBeforeFast(long firstPid, long secondPid) throws 
Exception {
+        CamelProcessorTop command = new CamelProcessorTop(new 
CamelJBangMain().withPrinter(printer));
+        command.sort = "name";
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph1 = mockProcessHandle(firstPid);
+            ProcessHandle ph2 = mockProcessHandle(secondPid);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            // fresh stream per invocation so allProcesses() can be called 
multiple times
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph1, ph2));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            int slowIdx = output.indexOf("slowRoute");
+            int fastIdx = output.indexOf("fastRoute");
+            assertTrue(slowIdx < fastIdx, "Slower processor (higher mean) 
should appear first regardless of stream order");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        CamelProcessorTop command = new CamelProcessorTop(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    private static JsonObject buildStatusWithOneProcessor(String contextName, 
String routeId, String mean) {
+        JsonArray routes = new JsonArray();
+        routes.add(processorEntry(routeId, mean));
+
+        JsonObject context = new JsonObject();
+        context.put("name", contextName);
+        context.put("phase", 5);
+
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("routes", routes);
+        return root;
+    }
+
+    private static JsonObject processorEntry(String routeId, String mean) {
+        JsonObject stats = new JsonObject();
+        stats.put("exchangesTotal", "10");
+        stats.put("exchangesFailed", "0");
+        stats.put("exchangesInflight", "0");
+        stats.put("meanProcessingTime", mean);
+        stats.put("maxProcessingTime", mean);
+        stats.put("minProcessingTime", mean);
+
+        JsonObject route = new JsonObject();
+        route.put("routeId", routeId);
+        route.put("from", "timer:tick");
+        route.put("state", "Started");
+        route.put("statistics", stats);
+        return route;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteGroupStatusTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteGroupStatusTest.java
new file mode 100644
index 000000000000..f1d287dec6f3
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteGroupStatusTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteGroupStatusTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testDisplaysRouteGroup() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithGroup("orderGroup", 
"Started", "10", "0"));
+
+        CamelRouteGroupStatus command = new CamelRouteGroupStatus(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("myApp"), "Should show integration 
name");
+            assertTrue(output.contains("orderGroup"), "Should show group 
name");
+            assertTrue(output.contains("Started"), "Should show group state");
+        }
+    }
+
+    @Test
+    void testRunningFilterExcludesSuspendedGroups() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithGroup("suspendedGroup", 
"Suspended", "0", "0"));
+
+        CamelRouteGroupStatus command = new CamelRouteGroupStatus(new 
CamelJBangMain().withPrinter(printer));
+        command.running = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "Suspended group 
should be hidden when --running is set");
+        }
+    }
+
+    @Test
+    void testFilterMeanExcludesFastGroups() throws Exception {
+        // group mean=5 should be filtered out when --filter-mean=20
+        writeStatusFile(TEST_PID, buildStatusWithGroup("fastGroup", "Started", 
"5", "0"));
+
+        CamelRouteGroupStatus command = new CamelRouteGroupStatus(new 
CamelJBangMain().withPrinter(printer));
+        command.mean = 20;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "Group below mean 
threshold should be filtered out");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        CamelRouteGroupStatus command = new CamelRouteGroupStatus(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testJsonOutput() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithGroup("shippingGroup", 
"Started", "30", "1"));
+
+        CamelRouteGroupStatus command = new CamelRouteGroupStatus(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.startsWith("["), "JSON output should be an 
array");
+            assertTrue(output.contains("shippingGroup"), "JSON should contain 
group name");
+        }
+    }
+
+    private static JsonObject buildStatusWithGroup(String groupName, String 
state, String mean, String failed) {
+        JsonObject stats = new JsonObject();
+        stats.put("exchangesTotal", "10");
+        stats.put("exchangesFailed", failed);
+        stats.put("exchangesInflight", "0");
+        stats.put("meanProcessingTime", mean);
+        stats.put("maxProcessingTime", mean);
+        stats.put("minProcessingTime", mean);
+
+        JsonArray routeIds = new JsonArray();
+        routeIds.add("route1");
+
+        JsonObject group = new JsonObject();
+        group.put("group", groupName);
+        group.put("size", 1);
+        group.put("routeIds", routeIds);
+        group.put("state", state);
+        group.put("uptime", "1m0s");
+        group.put("statistics", stats);
+
+        JsonArray groups = new JsonArray();
+        groups.add(group);
+
+        JsonObject context = new JsonObject();
+        context.put("name", "myApp");
+        context.put("phase", 5);
+
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("routeGroups", groups);
+        root.put("routes", new JsonArray());
+        return root;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteGroupTopTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteGroupTopTest.java
new file mode 100644
index 000000000000..a17b465b3981
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteGroupTopTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.stream.Stream;
+
+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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteGroupTopTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testSortsByMeanDescendingSlowerGroupFirst() throws Exception {
+        long slowPid = TEST_PID;
+        long fastPid = TEST_PID + 1;
+        // same context name so primary sort (name) ties and mean tiebreaker 
fires
+        writeStatusFile(slowPid, buildStatusWithOneGroup("myApp", "slowGroup", 
"150"));
+        writeStatusFile(fastPid, buildStatusWithOneGroup("myApp", "fastGroup", 
"5"));
+
+        // fast process first in stream — sort must correct the order
+        assertSlowBeforeFast(fastPid, slowPid);
+        // slow process first in stream — sort must preserve the order
+        printer = new StringPrinter();
+        assertSlowBeforeFast(slowPid, fastPid);
+    }
+
+    private void assertSlowBeforeFast(long firstPid, long secondPid) throws 
Exception {
+        CamelRouteGroupTop command = new CamelRouteGroupTop(new 
CamelJBangMain().withPrinter(printer));
+        command.sort = "name";
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph1 = mockProcessHandle(firstPid);
+            ProcessHandle ph2 = mockProcessHandle(secondPid);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            // fresh stream per invocation so allProcesses() can be called 
multiple times
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph1, ph2));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            int slowIdx = output.indexOf("slowGroup");
+            int fastIdx = output.indexOf("fastGroup");
+            assertTrue(slowIdx < fastIdx, "Slower group (higher mean) should 
appear first regardless of stream order");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        CamelRouteGroupTop command = new CamelRouteGroupTop(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    private static JsonObject buildStatusWithOneGroup(String contextName, 
String groupName, String mean) {
+        JsonArray groups = new JsonArray();
+        groups.add(groupEntry(groupName, mean));
+
+        JsonObject context = new JsonObject();
+        context.put("name", contextName);
+        context.put("phase", 5);
+
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("routeGroups", groups);
+        root.put("routes", new JsonArray());
+        return root;
+    }
+
+    private static JsonObject groupEntry(String groupName, String mean) {
+        JsonObject stats = new JsonObject();
+        stats.put("exchangesTotal", "10");
+        stats.put("exchangesFailed", "0");
+        stats.put("exchangesInflight", "0");
+        stats.put("meanProcessingTime", mean);
+        stats.put("maxProcessingTime", mean);
+        stats.put("minProcessingTime", mean);
+
+        JsonArray routeIds = new JsonArray();
+        routeIds.add("route1");
+
+        JsonObject group = new JsonObject();
+        group.put("group", groupName);
+        group.put("size", 1);
+        group.put("routeIds", routeIds);
+        group.put("state", "Started");
+        group.put("uptime", "1m0s");
+        group.put("statistics", stats);
+        return group;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteTopTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteTopTest.java
new file mode 100644
index 000000000000..4860ac697953
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteTopTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.stream.Stream;
+
+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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteTopTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testDisplaysRouteRow() throws Exception {
+        writeStatusFile(TEST_PID, buildRouteStatus("myRoute", "Started"));
+
+        CamelRouteTop command = new CamelRouteTop(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertTrue(printer.getOutput().contains("myRoute"), "Should 
display the route ID");
+        }
+    }
+
+    @Test
+    void testSortsByMeanDescendingSlowerRouteFirst() throws Exception {
+        long slowPid = TEST_PID;
+        long fastPid = TEST_PID + 1;
+        // same context name so primary sort (name) ties and mean tiebreaker 
fires
+        writeStatusFile(slowPid, buildStatusWithOneRoute("myApp", "slowRoute", 
"300"));
+        writeStatusFile(fastPid, buildStatusWithOneRoute("myApp", "fastRoute", 
"8"));
+
+        // fast process first in stream — sort must correct the order
+        assertSlowBeforeFast(fastPid, slowPid);
+        // slow process first in stream — sort must preserve the order
+        printer = new StringPrinter();
+        assertSlowBeforeFast(slowPid, fastPid);
+    }
+
+    private void assertSlowBeforeFast(long firstPid, long secondPid) throws 
Exception {
+        CamelRouteTop command = new CamelRouteTop(new 
CamelJBangMain().withPrinter(printer));
+        command.sort = "name";
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph1 = mockProcessHandle(firstPid);
+            ProcessHandle ph2 = mockProcessHandle(secondPid);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            // fresh stream per invocation so allProcesses() can be called 
multiple times
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph1, ph2));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            int slowIdx = output.indexOf("slowRoute");
+            int fastIdx = output.indexOf("fastRoute");
+            assertTrue(slowIdx < fastIdx, "Slower route (higher mean) should 
appear first regardless of stream order");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        CamelRouteTop command = new CamelRouteTop(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    private static JsonObject buildStatusWithOneRoute(String contextName, 
String routeId, String mean) {
+        JsonObject stats = new JsonObject();
+        stats.put("exchangesTotal", "10");
+        stats.put("exchangesFailed", "0");
+        stats.put("exchangesInflight", "0");
+        stats.put("meanProcessingTime", mean);
+        stats.put("maxProcessingTime", mean);
+        stats.put("minProcessingTime", mean);
+
+        JsonObject route = new JsonObject();
+        route.put("routeId", routeId);
+        route.put("from", "timer:tick");
+        route.put("state", "Started");
+        route.put("uptime", "1m");
+        route.put("statistics", stats);
+
+        JsonArray routes = new JsonArray();
+        routes.add(route);
+
+        JsonObject context = new JsonObject();
+        context.put("name", contextName);
+        context.put("phase", 5);
+
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        root.put("routes", routes);
+        return root;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/DirtyTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/DirtyTest.java
new file mode 100644
index 000000000000..38f962e1f432
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/DirtyTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class DirtyTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testReturnsZeroWhenNoFiles() throws Exception {
+        // camel dir is empty: Dirty returns before touching ProcessHandle
+        Dirty command = new Dirty(new CamelJBangMain().withPrinter(printer));
+        int exit = command.doCall();
+        assertEquals(0, exit);
+        assertEquals("", printer.getOutput().trim());
+    }
+
+    @Test
+    void testDetectsOrphanFile() throws Exception {
+        // write a status file for a PID that has no running process
+        Path orphan = 
CommandLineHelper.getCamelDir().resolve("77777-status.json");
+        Files.writeString(orphan, "{}");
+
+        Dirty command = new Dirty(new CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            // no running processes -> orphan file is not matched
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertTrue(printer.getOutput().contains("orphan"), "Should report 
orphan file count");
+        }
+    }
+
+    @Test
+    void testRunningProcessFileIsNotDirty() throws Exception {
+        // status file for TEST_PID is owned by a running process -> not dirty
+        writeStatusFile(TEST_PID, buildContextStatus("liveApp", 5));
+
+        Dirty command = new Dirty(new CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "File belonging to a 
running PID is not dirty");
+        }
+    }
+
+    @Test
+    void testCleanFlagDeletesOrphanFile() throws Exception {
+        Path orphan = 
CommandLineHelper.getCamelDir().resolve("88888-status.json");
+        Files.writeString(orphan, "{}");
+        assertTrue(Files.exists(orphan), "Orphan file must exist before 
cleaning");
+
+        Dirty command = new Dirty(new CamelJBangMain().withPrinter(printer));
+        command.clean = true;
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertFalse(Files.exists(orphan), "Orphan file should be deleted 
by --clean");
+            assertTrue(printer.getOutput().contains("Cleaned"), "Should print 
cleaned confirmation");
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListGroovyTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListGroovyTest.java
new file mode 100644
index 000000000000..c774cda08aef
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListGroovyTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class ListGroovyTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testDisplaysGroovyCompilerStats() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithGroovy(3, 1, "MyRoute", 
"MyBean"));
+
+        ListGroovy command = new ListGroovy(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("myApp"), "Should show integration 
name");
+            assertTrue(output.contains("MyRoute"), "Should show compiled class 
names");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoGroovySection() throws Exception {
+        JsonObject status = buildContextStatus("noGroovyApp", 5);
+        writeStatusFile(TEST_PID, status);
+
+        ListGroovy command = new ListGroovy(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "No groovy section 
means no output rows");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        ListGroovy command = new ListGroovy(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testJsonOutput() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithGroovy(2, 0, "MyProcessor"));
+
+        ListGroovy command = new ListGroovy(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.startsWith("["), "JSON output should be an 
array");
+            assertTrue(output.contains("compile"), "JSON should contain 
compile field");
+        }
+    }
+
+    private static JsonObject buildStatusWithGroovy(int compiled, int 
preloaded, String... classNames) {
+        JsonArray classes = new JsonArray();
+        for (String name : classNames) {
+            classes.add(name);
+        }
+
+        JsonObject compiler = new JsonObject();
+        compiler.put("compiledCounter", compiled);
+        compiler.put("preloadedCounter", preloaded);
+        compiler.put("classesSize", classNames.length);
+        compiler.put("compiledTime", 1500L);
+        compiler.put("lastCompilationTimestamp", 0L);
+        compiler.put("classes", classes);
+
+        JsonObject groovy = new JsonObject();
+        groovy.put("compiler", compiler);
+
+        JsonObject root = buildContextStatus("myApp", 5);
+        root.put("groovy", groovy);
+        return root;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListKafkaTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListKafkaTest.java
new file mode 100644
index 000000000000..4033000a9f33
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListKafkaTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class ListKafkaTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testDisplaysKafkaConsumer() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithKafkaConsumer("myRoute", 
"kafka:orders", "my-group", "orders", 0, 42L));
+
+        ListKafka command = new ListKafka(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("myApp"), "Should show integration 
name");
+            assertTrue(output.contains("orders"), "Should show topic name");
+            assertTrue(output.contains("my-group"), "Should show consumer 
group");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoKafkaSection() throws Exception {
+        JsonObject status = buildContextStatus("noKafkaApp", 5);
+        writeStatusFile(TEST_PID, status);
+
+        ListKafka command = new ListKafka(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "No kafka section 
means no output rows");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        ListKafka command = new ListKafka(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testJsonOutput() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithKafkaConsumer("myRoute", 
"kafka:events", "event-group", "events", 1, 7L));
+
+        ListKafka command = new ListKafka(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.startsWith("["), "JSON output should be an 
array");
+            assertTrue(output.contains("events"), "JSON should contain topic 
name");
+        }
+    }
+
+    private static JsonObject buildStatusWithKafkaConsumer(
+            String routeId, String uri, String groupId, String topic, int 
partition, long offset) {
+        JsonObject worker = new JsonObject();
+        worker.put("threadId", "thread-1");
+        worker.put("state", "Running");
+        worker.put("groupId", groupId);
+        worker.put("lastTopic", topic);
+        worker.put("lastPartition", partition);
+        worker.put("lastOffset", offset);
+
+        JsonArray workers = new JsonArray();
+        workers.add(worker);
+
+        JsonObject consumer = new JsonObject();
+        consumer.put("routeId", routeId);
+        consumer.put("uri", uri);
+        consumer.put("state", "Running");
+        consumer.put("workers", workers);
+
+        JsonArray consumers = new JsonArray();
+        consumers.add(consumer);
+
+        JsonObject kafka = new JsonObject();
+        kafka.put("kafkaConsumers", consumers);
+
+        JsonObject root = buildContextStatus("myApp", 5);
+        root.put("kafka", kafka);
+        return root;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListPlatformHttpTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListPlatformHttpTest.java
new file mode 100644
index 000000000000..08f84bbdb5ff
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListPlatformHttpTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+@ExtendWith(MockitoExtension.class)
+class ListPlatformHttpTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testDisplaysEndpoint() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithHttp("/api/orders", "GET", 
"/q/health", "GET"));
+
+        ListPlatformHttp command = new ListPlatformHttp(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("/api/orders"), "Should show user 
endpoint path");
+            assertTrue(output.contains("GET"), "Should show HTTP verb");
+        }
+    }
+
+    @Test
+    void testManagementEndpointsHiddenByDefault() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithHttp("/api/orders", "GET", 
"/q/health", "GET"));
+
+        ListPlatformHttp command = new ListPlatformHttp(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertFalse(printer.getOutput().contains("/q/health"),
+                    "Management endpoint should be hidden without --all");
+        }
+    }
+
+    @Test
+    void testAllFlagIncludesManagementEndpoints() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithHttp("/api/orders", "POST", 
"/q/health", "GET"));
+
+        ListPlatformHttp command = new ListPlatformHttp(new 
CamelJBangMain().withPrinter(printer));
+        command.sort = "pid";
+        command.all = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.contains("/api/orders"), "Should show user 
endpoint");
+            assertTrue(output.contains("/q/health"), "Should show management 
endpoint when --all");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoPlatformHttpSection() throws Exception {
+        JsonObject status = buildContextStatus("noHttpApp", 5);
+        writeStatusFile(TEST_PID, status);
+
+        ListPlatformHttp command = new ListPlatformHttp(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "No platform-http 
section means no output rows");
+        }
+    }
+
+    @Test
+    void testEmptyOutputWhenNoProcesses() throws Exception {
+        ListPlatformHttp command = new ListPlatformHttp(new 
CamelJBangMain().withPrinter(printer));
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim());
+        }
+    }
+
+    @Test
+    void testJsonOutput() throws Exception {
+        writeStatusFile(TEST_PID, buildStatusWithHttp("/api/ping", "GET", 
"/q/health", "GET"));
+
+        ListPlatformHttp command = new ListPlatformHttp(new 
CamelJBangMain().withPrinter(printer));
+        command.jsonOutput = true;
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            String output = printer.getOutput();
+            assertTrue(output.startsWith("["), "JSON output should be an 
array");
+            // Jsoner escapes forward slashes, so assert on the slug without 
them
+            assertTrue(output.contains("api") && output.contains("ping"), 
"JSON should contain endpoint path segments");
+            assertTrue(output.contains("method"), "JSON should contain method 
field");
+        }
+    }
+
+    private static JsonObject buildStatusWithHttp(
+            String userPath, String userVerb, String mgmtPath, String 
mgmtVerb) {
+        JsonObject userEndpoint = new JsonObject();
+        userEndpoint.put("url", "http://localhost:8080"; + userPath);
+        userEndpoint.put("path", userPath);
+        userEndpoint.put("verbs", userVerb);
+
+        JsonArray endpoints = new JsonArray();
+        endpoints.add(userEndpoint);
+
+        JsonObject mgmtEndpoint = new JsonObject();
+        mgmtEndpoint.put("url", "http://localhost:8080"; + mgmtPath);
+        mgmtEndpoint.put("path", mgmtPath);
+        mgmtEndpoint.put("verbs", mgmtVerb);
+
+        JsonArray mgmtEndpoints = new JsonArray();
+        mgmtEndpoints.add(mgmtEndpoint);
+
+        JsonObject platformHttp = new JsonObject();
+        platformHttp.put("server", "netty");
+        platformHttp.put("endpoints", endpoints);
+        platformHttp.put("managementEndpoints", mgmtEndpoints);
+
+        JsonObject root = buildContextStatus("myApp", 5);
+        root.put("platform-http", platformHttp);
+        return root;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProcessTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProcessTest.java
index 4c277d82bee5..b1a4681c67a6 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProcessTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProcessTest.java
@@ -20,6 +20,7 @@ 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.StringPrinter;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -170,32 +171,40 @@ class ListProcessTest extends ProcessCommandTestSupport {
 
     @Test
     void testSortByName() throws Exception {
-        long pid1 = 11111L;
-        long pid2 = 22222L;
+        long zebraPid = 11111L;
+        long applePid = 22222L;
 
         JsonObject status1 = buildContextStatus("zebra", 5);
         JsonObject status2 = buildContextStatus("apple", 5);
-        writeStatusFile(pid1, status1);
-        writeStatusFile(pid2, status2);
+        writeStatusFile(zebraPid, status1);
+        writeStatusFile(applePid, status2);
+
+        // zebra process first in stream — sort must promote apple to top
+        assertAppleBeforeZebra(zebraPid, applePid);
+        // apple process first in stream — sort must preserve the order
+        printer = new StringPrinter();
+        assertAppleBeforeZebra(applePid, zebraPid);
+    }
 
+    private void assertAppleBeforeZebra(long firstPid, long secondPid) throws 
Exception {
         ListProcess command = new ListProcess(new 
CamelJBangMain().withPrinter(printer));
         command.sort = "name";
 
         try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
-            ProcessHandle ph1 = mockProcessHandle(pid1);
-            ProcessHandle ph2 = mockProcessHandle(pid2);
+            ProcessHandle ph1 = mockProcessHandle(firstPid);
+            ProcessHandle ph2 = mockProcessHandle(secondPid);
             ProcessHandle currentHandle = mockCurrentHandle();
             mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+            // fresh stream per invocation so allProcesses() can be called 
multiple times
             mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph1, ph2));
 
             int exit = command.doCall();
 
             assertEquals(0, exit);
             String output = printer.getOutput();
-            // apple should appear before zebra when sorted ascending by name
             int applePos = output.indexOf("apple");
             int zebraPos = output.indexOf("zebra");
-            assertTrue(applePos < zebraPos, "Sort by name ascending: apple 
must precede zebra");
+            assertTrue(applePos < zebraPos, "Sort by name ascending: apple 
must precede zebra regardless of stream order");
         }
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcessTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcessTest.java
new file mode 100644
index 000000000000..c8aa610f2f94
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/RestartProcessTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+
+// TODO: add tests for the successful restart path once 
ProcessBuilder/Runtime.exec can be mocked
+//       or an integration-test harness is available. The current tests only 
cover early-exit guards.
+@ExtendWith(MockitoExtension.class)
+class RestartProcessTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testReturnsErrorWhenNoMatchingProcess() throws Exception {
+        RestartProcess command = new RestartProcess(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "nonExistentApp";
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(1, exit, "Should return non-zero when no process 
matches");
+            assertTrue(printer.getOutput().contains("No matching"), "Should 
print no-match message");
+        }
+    }
+
+    @Test
+    void testReturnsErrorWhenMultipleProcessesMatch() throws Exception {
+        long pid2 = TEST_PID + 1;
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        writeStatusFile(pid2, buildContextStatus("myApp", 5));
+
+        RestartProcess command = new RestartProcess(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph1 = mockProcessHandle(TEST_PID);
+            ProcessHandle ph2 = mockProcessHandle(pid2);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.of(ph1, ph2));
+
+            int exit = command.doCall();
+
+            assertEquals(1, exit, "Should return non-zero when multiple 
processes match");
+            assertTrue(printer.getOutput().contains("Multiple"), "Should print 
ambiguity message");
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/StopProcessTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/StopProcessTest.java
new file mode 100644
index 000000000000..db63aecea085
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/StopProcessTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class StopProcessTest extends ProcessCommandTestSupport {
+
+    @Test
+    void testStopDeletesPidFile() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+        // the pid file (named by PID only) signals graceful shutdown when 
deleted
+        Path pidFile = 
CommandLineHelper.getCamelDir().resolve(Long.toString(TEST_PID));
+        Files.writeString(pidFile, "");
+        assertTrue(Files.exists(pidFile), "Pid file must exist before stop");
+
+        StopProcess command = new StopProcess(new 
CamelJBangMain().withPrinter(printer));
+
+        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));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertFalse(Files.exists(pidFile), "Pid file should be deleted to 
signal graceful stop");
+            assertTrue(printer.getOutput().contains("Shutting down"), "Should 
print shutdown message");
+        }
+    }
+
+    @Test
+    void testStopWithKillCallsDestroyForcibly() throws Exception {
+        writeStatusFile(TEST_PID, buildContextStatus("myApp", 5));
+
+        StopProcess command = new StopProcess(new 
CamelJBangMain().withPrinter(printer));
+        command.kill = true;
+
+        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));
+            mocked.when(() -> 
ProcessHandle.of(TEST_PID)).thenReturn(Optional.of(ph));
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            verify(ph).destroyForcibly();
+            assertTrue(printer.getOutput().contains("Killing"), "Should print 
kill message");
+        }
+    }
+
+    @Test
+    void testStopWithNoMatchingProcessDoesNothing() throws Exception {
+        StopProcess command = new StopProcess(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "nonExistentApp";
+
+        try (MockedStatic<ProcessHandle> mocked = 
mockStatic(ProcessHandle.class)) {
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> 
Stream.empty());
+
+            int exit = command.doCall();
+
+            assertEquals(0, exit);
+            assertEquals("", printer.getOutput().trim(), "No output expected 
when no processes match");
+        }
+    }
+}


Reply via email to