You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/06/08 07:40:35 UTC
(camel) branch main updated: CAMEL-23688: Add extensive test coverage for camel-jbang CLI commands
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 15fd9819fdfa CAMEL-23688: Add extensive test coverage for camel-jbang CLI commands
15fd9819fdfa is described below
commit 15fd9819fdfaba380a53837167e5abb7fcead560
Author: Adriano Machado <60...@users.noreply.github.com>
AuthorDate: Mon Jun 8 03:40:28 2026 -0400
CAMEL-23688: Add extensive test coverage for camel-jbang CLI commands
Add 24 new test classes covering camel-jbang-core CLI commands that
previously had zero or minimal test coverage. Tests cover the
commands/process/ package (ListProcess, CamelContextStatus,
CamelRouteStatus, ListEndpoint, ListHealth, ListRest, ListConsumer,
ListError, ListVariable, ListMetric, ListCircuitBreaker, ListBlocked,
ListEvent, ListInflight, ListInternalTask, ListProducer, ListService,
ListTransformer, ListProperties), commands/action/ package
(CamelStartupRecorderAction, LoggerAction, MessageTableHelper,
CamelSourceAction), and CamelCommandHelper. Uses Mockito mockStatic
for ProcessHandle mocking with hand-crafted JSON status file fixtures.
Closes #23812
---
.../core/commands/CamelCommandHelperTest.java | 65 ++++++
.../commands/action/CamelSourceActionTest.java | 60 +++++
.../action/CamelStartupRecorderActionTest.java | 195 ++++++++++++++++
.../core/commands/action/LoggerActionTest.java | 197 ++++++++++++++++
.../commands/action/MessageTableHelperTest.java | 116 ++++++++++
.../commands/process/CamelContextStatusTest.java | 147 ++++++++++++
.../commands/process/CamelRouteStatusTest.java | 255 +++++++++++++++++++++
.../core/commands/process/ListBlockedTest.java | 133 +++++++++++
.../commands/process/ListCircuitBreakerTest.java | 159 +++++++++++++
.../core/commands/process/ListConsumerTest.java | 139 +++++++++++
.../core/commands/process/ListEndpointTest.java | 160 +++++++++++++
.../jbang/core/commands/process/ListErrorTest.java | 133 +++++++++++
.../jbang/core/commands/process/ListEventTest.java | 165 +++++++++++++
.../core/commands/process/ListHealthTest.java | 174 ++++++++++++++
.../core/commands/process/ListInflightTest.java | 137 +++++++++++
.../commands/process/ListInternalTaskTest.java | 139 +++++++++++
.../core/commands/process/ListMetricTest.java | 132 +++++++++++
.../core/commands/process/ListProcessTest.java | 238 +++++++++++++++++++
.../core/commands/process/ListProducerTest.java | 194 ++++++++++++++++
.../core/commands/process/ListPropertiesTest.java | 173 ++++++++++++++
.../jbang/core/commands/process/ListRestTest.java | 135 +++++++++++
.../core/commands/process/ListServiceTest.java | 174 ++++++++++++++
.../core/commands/process/ListTransformerTest.java | 132 +++++++++++
.../core/commands/process/ListVariableTest.java | 131 +++++++++++
24 files changed, 3683 insertions(+)
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelCommandHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelCommandHelperTest.java
new file mode 100644
index 000000000000..0ce42940e136
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelCommandHelperTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.common.CamelCommandHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CamelCommandHelperTest {
+
+ @Test
+ void testStartingState() {
+ // Phase <= 4 maps to Starting
+ assertEquals("Starting", CamelCommandHelper.extractState(0));
+ assertEquals("Starting", CamelCommandHelper.extractState(1));
+ assertEquals("Starting", CamelCommandHelper.extractState(4));
+ }
+
+ @Test
+ void testRunningState() {
+ assertEquals("Running", CamelCommandHelper.extractState(5));
+ }
+
+ @Test
+ void testSuspendingState() {
+ assertEquals("Suspending", CamelCommandHelper.extractState(6));
+ }
+
+ @Test
+ void testSuspendedState() {
+ assertEquals("Suspended", CamelCommandHelper.extractState(7));
+ }
+
+ @Test
+ void testTerminatingState() {
+ assertEquals("Terminating", CamelCommandHelper.extractState(8));
+ }
+
+ @Test
+ void testTerminatedState() {
+ assertEquals("Terminated", CamelCommandHelper.extractState(9));
+ }
+
+ @Test
+ void testUnknownPhaseFallsToTerminated() {
+ // Any phase beyond 9 defaults to Terminated — prevents NPE during display
+ assertEquals("Terminated", CamelCommandHelper.extractState(99));
+ assertEquals("Terminated", CamelCommandHelper.extractState(Integer.MAX_VALUE));
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceActionTest.java
new file mode 100644
index 000000000000..cdfdbf13c262
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceActionTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class CamelSourceActionTest {
+
+ @Test
+ void testNullReturnsNull() {
+ assertNull(CamelSourceAction.extractSourceName(null));
+ }
+
+ @Test
+ void testPlainFilenameUnchanged() {
+ assertEquals("MyRoute.yaml", CamelSourceAction.extractSourceName("MyRoute.yaml"));
+ }
+
+ @Test
+ void testSchemeAndPathStripsSchemeAndDirectory() {
+ // "file:/some/path/MyRoute.yaml" -> strip scheme -> "/some/path/MyRoute.yaml" -> strip path -> "MyRoute.yaml"
+ assertEquals("MyRoute.yaml", CamelSourceAction.extractSourceName("file:/some/path/MyRoute.yaml"));
+ }
+
+ @Test
+ void testClasspathSchemeStripsScheme() {
+ assertEquals("routes.xml", CamelSourceAction.extractSourceName("classpath:routes.xml"));
+ }
+
+ @Test
+ void testSingleColonTreatedAsScheme() {
+ // Only one colon: treated as scheme separator, not line number.
+ // "MyRoute.java:42" -> substring after ':' = "42" -> stripPath("42") = "42"
+ assertEquals("42", CamelSourceAction.extractSourceName("MyRoute.java:42"));
+ }
+
+ @Test
+ void testSchemeWithLineNumberStripsSchemeAndLineNumber() {
+ // Two colons: stripSourceLocationLineNumber removes ":10" -> "file:/path/MyRoute.yaml"
+ // then strip scheme and path -> "MyRoute.yaml"
+ assertEquals("MyRoute.yaml", CamelSourceAction.extractSourceName("file:/path/MyRoute.yaml:10"));
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStartupRecorderActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStartupRecorderActionTest.java
new file mode 100644
index 000000000000..3cbdb9fc3a05
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStartupRecorderActionTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.Path;
+import java.util.Collections;
+import java.util.List;
+
+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.CommandLineHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CamelStartupRecorderActionTest extends CamelCommandBaseTestSupport {
+
+ @BeforeEach
+ @Override
+ public void setup() throws Exception {
+ super.setup();
+ CommandLineHelper.useHomeDir("target");
+ }
+
+ @Test
+ void testDisplaysStepsFromOutput() throws Exception {
+ JsonObject mockOutput = buildStartupOutput(
+ step(1, 0, 0, "MyRoute", "Route", "Build route", 150),
+ step(2, 1, 1, null, "From", "timer:tick", 50),
+ step(3, 1, 1, null, "To", "log:info", 30));
+
+ TestableStartupRecorder command = new TestableStartupRecorder(
+ new CamelJBangMain().withPrinter(printer), mockOutput);
+
+ int exit = command.doWatchCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("Route"), "Should show step type Route");
+ assertTrue(output.contains("From"), "Should show step type From");
+ assertTrue(output.contains("Build route"), "Should show step description");
+ }
+
+ @Test
+ void testStepWithNameShowsParentheses() throws Exception {
+ JsonObject mockOutput = buildStartupOutput(
+ step(1, 0, 0, "myRoute", "Route", "Configure route", 100));
+
+ TestableStartupRecorder command = new TestableStartupRecorder(
+ new CamelJBangMain().withPrinter(printer), mockOutput);
+
+ int exit = command.doWatchCall();
+
+ assertEquals(0, exit);
+ // When a step has a name, it appears as "description(name)"
+ assertTrue(printer.getOutput().contains("Configure route(myRoute)"),
+ "Name should be appended in parentheses");
+ }
+
+ @Test
+ void testSortByDuration() throws Exception {
+ JsonObject mockOutput = buildStartupOutput(
+ step(1, 0, 0, null, "Route", "FastStep", 10),
+ step(2, 0, 0, null, "Route", "SlowStep", 500),
+ step(3, 0, 0, null, "Route", "MidStep", 200));
+
+ TestableStartupRecorder command = new TestableStartupRecorder(
+ new CamelJBangMain().withPrinter(printer), mockOutput);
+ command.sort = "duration";
+
+ int exit = command.doWatchCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ int fastPos = output.indexOf("FastStep");
+ int midPos = output.indexOf("MidStep");
+ int slowPos = output.indexOf("SlowStep");
+ // Ascending duration: FastStep (10ms) before MidStep (200ms) before SlowStep (500ms)
+ assertTrue(fastPos < midPos && midPos < slowPos,
+ "Sort by duration ascending: fastest step must appear first");
+ }
+
+ @Test
+ void testSortByDurationDescending() throws Exception {
+ JsonObject mockOutput = buildStartupOutput(
+ step(1, 0, 0, null, "Route", "FastStep", 10),
+ step(2, 0, 0, null, "Route", "SlowStep", 500));
+
+ TestableStartupRecorder command = new TestableStartupRecorder(
+ new CamelJBangMain().withPrinter(printer), mockOutput);
+ command.sort = "-duration";
+
+ int exit = command.doWatchCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ int slowPos = output.indexOf("SlowStep");
+ int fastPos = output.indexOf("FastStep");
+ assertTrue(slowPos < fastPos, "Descending duration: slowest step must appear first");
+ }
+
+ @Test
+ void testZeroDurationDisplayedAsEmpty() throws Exception {
+ JsonObject mockOutput = buildStartupOutput(
+ step(1, 0, 0, null, "Route", "ZeroDurationStep", 0));
+
+ TestableStartupRecorder command = new TestableStartupRecorder(
+ new CamelJBangMain().withPrinter(printer), mockOutput);
+
+ int exit = command.doWatchCall();
+
+ assertEquals(0, exit);
+ // Zero duration is rendered as empty string, not "0" — verify step name still appears
+ assertTrue(printer.getOutput().contains("ZeroDurationStep"));
+ }
+
+ @Test
+ void testEmptyOutputWhenNoPidFound() throws Exception {
+ // No pids returned — command should return error code 1
+ TestableStartupRecorder command = new TestableStartupRecorder(
+ new CamelJBangMain().withPrinter(printer), null) {
+ @Override
+ List<Long> findPids(String name) {
+ return List.of();
+ }
+ };
+
+ int exit = command.doWatchCall();
+
+ assertEquals(1, exit, "Should return error when no pids found");
+ }
+
+ private static JsonObject buildStartupOutput(JsonObject... steps) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, steps);
+ JsonObject jo = new JsonObject();
+ jo.put("steps", arr);
+ return jo;
+ }
+
+ private static JsonObject step(
+ int id, int parentId, int level, String name, String type, String description,
+ long duration) {
+ JsonObject step = new JsonObject();
+ step.put("id", id);
+ step.put("parentId", parentId);
+ step.put("level", level);
+ step.put("name", name);
+ step.put("type", type);
+ step.put("description", description);
+ step.put("duration", duration);
+ return step;
+ }
+
+ /**
+ * Test subclass that bypasses process discovery and output file waiting so the rendering logic can be tested
+ * against a pre-built JSON response.
+ */
+ private static class TestableStartupRecorder extends CamelStartupRecorderAction {
+ private final JsonObject mockOutput;
+
+ TestableStartupRecorder(CamelJBangMain main, JsonObject mockOutput) {
+ super(main);
+ this.mockOutput = mockOutput;
+ }
+
+ @Override
+ List<Long> findPids(String name) {
+ return List.of(12345L);
+ }
+
+ @Override
+ protected JsonObject waitForOutputFile(Path outputFile) {
+ return mockOutput;
+ }
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/LoggerActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/LoggerActionTest.java
new file mode 100644
index 000000000000..8fedd383260c
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/LoggerActionTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+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.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+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.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class LoggerActionTest extends CamelCommandBaseTestSupport {
+
+ private static final long TEST_PID = 12345L;
+ private static final long CURRENT_PID = 99999L;
+
+ @BeforeEach
+ @Override
+ public void setup() throws Exception {
+ super.setup();
+ CommandLineHelper.useHomeDir("target/test-logger");
+ Files.createDirectories(CommandLineHelper.getCamelDir());
+ }
+
+ @AfterEach
+ void cleanup() throws IOException {
+ Path camelDir = CommandLineHelper.getCamelDir();
+ if (Files.exists(camelDir)) {
+ try (var entries = Files.list(camelDir)) {
+ for (Path p : entries.toList()) {
+ Files.deleteIfExists(p);
+ }
+ }
+ Files.deleteIfExists(camelDir);
+ }
+ }
+
+ @Test
+ void testListShowsLoggers() throws Exception {
+ JsonObject levels = new JsonObject();
+ levels.put("org.apache.camel", "INFO");
+ levels.put("com.example", "DEBUG");
+
+ JsonObject logger = new JsonObject();
+ logger.put("levels", levels);
+
+ JsonObject context = new JsonObject();
+ context.put("name", "myApp");
+
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ root.put("logger", logger);
+ writeStatusFile(TEST_PID, root);
+
+ LoggerAction command = new LoggerAction(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockHandle(TEST_PID);
+ ProcessHandle current = mockHandle(CURRENT_PID);
+ 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("org.apache.camel"), "Should show logger name");
+ assertTrue(output.contains("INFO"), "Should show logger level");
+ assertTrue(output.contains("com.example"), "Should list all loggers");
+ assertTrue(output.contains("DEBUG"), "Should show DEBUG level");
+ }
+ }
+
+ @Test
+ void testEmptyOutputWhenNoLoggers() throws Exception {
+ JsonObject context = new JsonObject();
+ context.put("name", "myApp");
+
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ // No logger section
+ writeStatusFile(TEST_PID, root);
+
+ LoggerAction command = new LoggerAction(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockHandle(TEST_PID);
+ ProcessHandle current = mockHandle(CURRENT_PID);
+ 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());
+ }
+ }
+
+ @Test
+ void testSortByName() throws Exception {
+ // "sort=name" sorts rows by context/process name; use two processes with different names
+ long pidZzz = TEST_PID;
+ long pidAaa = TEST_PID + 1;
+
+ JsonObject levels = new JsonObject();
+ levels.put("root", "INFO");
+
+ JsonObject logger = new JsonObject();
+ logger.put("levels", levels);
+
+ JsonObject rootZzz = new JsonObject();
+ rootZzz.put("context", contextJson("zzzApp"));
+ rootZzz.put("logger", logger);
+ writeStatusFile(pidZzz, rootZzz);
+
+ JsonObject rootAaa = new JsonObject();
+ rootAaa.put("context", contextJson("aaaApp"));
+ rootAaa.put("logger", logger);
+ writeStatusFile(pidAaa, rootAaa);
+
+ LoggerAction command = new LoggerAction(new CamelJBangMain().withPrinter(printer));
+ command.sort = "name";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle phZzz = mockHandle(pidZzz);
+ ProcessHandle phAaa = mockHandle(pidAaa);
+ ProcessHandle current = mockHandle(CURRENT_PID);
+ mocked.when(ProcessHandle::current).thenReturn(current);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(phZzz, phAaa));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ int aaaPos = output.indexOf("aaaApp");
+ int zzzPos = output.indexOf("zzzApp");
+ assertTrue(aaaPos < zzzPos, "Sort by name ascending: aaaApp must appear before zzzApp");
+ }
+ }
+
+ private static JsonObject contextJson(String name) {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", name);
+ return ctx;
+ }
+
+ private static ProcessHandle mockHandle(long pid) {
+ ProcessHandle ph = mock(ProcessHandle.class);
+ ProcessHandle.Info info = mock(ProcessHandle.Info.class);
+ when(ph.pid()).thenReturn(pid);
+ // info/commandLine/startInstant are only used for process handles, not for the current handle
+ lenient().when(ph.info()).thenReturn(info);
+ lenient().when(info.commandLine()).thenReturn(Optional.empty());
+ lenient().when(info.startInstant()).thenReturn(Optional.of(Instant.now().minusSeconds(60)));
+ return ph;
+ }
+
+ private static void writeStatusFile(long pid, JsonObject root) throws Exception {
+ Path f = CommandLineHelper.getCamelDir().resolve(pid + "-status.json");
+ Files.writeString(f, root.toJson());
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelperTest.java
new file mode 100644
index 000000000000..9e41b1ae9f94
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelperTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class MessageTableHelperTest {
+
+ @Test
+ void testBodyAppearsInOutput() {
+ MessageTableHelper helper = new MessageTableHelper();
+
+ JsonObject root = new JsonObject();
+ root.put("exchangeType", "DefaultExchange");
+ root.put("messageType", "DefaultMessage");
+
+ JsonObject body = new JsonObject();
+ body.put("type", "String");
+ body.put("value", "Hello World");
+ root.put("body", body);
+
+ String result = helper.getDataAsTable("exch-1", "InOnly", null, null, null, root, null);
+
+ assertNotNull(result, "Result should not be null");
+ assertTrue(result.contains("Hello World"), "Output should contain the body value");
+ }
+
+ @Test
+ void testHeadersAppearsWhenEnabled() {
+ MessageTableHelper helper = new MessageTableHelper();
+ helper.setShowHeaders(true);
+
+ JsonObject root = new JsonObject();
+ root.put("exchangeType", "DefaultExchange");
+
+ JsonArray headers = new JsonArray();
+ JsonObject header = new JsonObject();
+ header.put("key", "Content-Type");
+ header.put("type", "String");
+ header.put("value", "application/json");
+ headers.add(header);
+ root.put("headers", headers);
+
+ String result = helper.getDataAsTable("exch-2", "InOnly", null, null, null, root, null);
+
+ assertNotNull(result);
+ assertTrue(result.contains("Content-Type"), "Output should contain header name when showHeaders=true");
+ assertTrue(result.contains("application/json"), "Output should contain header value");
+ }
+
+ @Test
+ void testHeadersHiddenWhenDisabled() {
+ MessageTableHelper helper = new MessageTableHelper();
+ helper.setShowHeaders(false);
+
+ JsonObject root = new JsonObject();
+ root.put("exchangeType", "DefaultExchange");
+
+ JsonArray headers = new JsonArray();
+ JsonObject header = new JsonObject();
+ header.put("key", "X-Custom-Header");
+ header.put("type", "String");
+ header.put("value", "hidden-value");
+ headers.add(header);
+ root.put("headers", headers);
+
+ String result = helper.getDataAsTable("exch-3", "InOnly", null, null, null, root, null);
+
+ assertNotNull(result);
+ assertFalse(result.contains("X-Custom-Header"), "Headers should not appear when showHeaders=false");
+
+ }
+
+ @Test
+ void testNullRootReturnsEmptyTable() {
+ MessageTableHelper helper = new MessageTableHelper();
+
+ String result = helper.getDataAsTable("exch-4", "InOnly", null, null, null, null, null);
+
+ assertNotNull(result, "Result should not be null even for null root");
+ assertTrue(result.isBlank(), "Should return empty output when root is null");
+ }
+
+ @Test
+ void testExchangeIdAppearsInOutput() {
+ MessageTableHelper helper = new MessageTableHelper();
+
+ JsonObject root = new JsonObject();
+ root.put("exchangeType", "DefaultExchange");
+
+ String result = helper.getDataAsTable("my-exchange-id-12345", "InOut", null, null, null, root, null);
+
+ assertNotNull(result);
+ assertTrue(result.contains("my-exchange-id-12345"), "Exchange ID should appear in output");
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatusTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatusTest.java
new file mode 100644
index 000000000000..801cb0077482
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelContextStatusTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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 CamelContextStatusTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testDisplaysRunningContext() throws Exception {
+ JsonObject status = buildContextStatus("myApp", 5);
+ writeStatusFile(TEST_PID, status);
+
+ CamelContextStatus command = new CamelContextStatus(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 display context name");
+ assertTrue(output.contains("Running"), "Phase 5 should show Running status");
+ }
+ }
+
+ @Test
+ void testDisplaysSuspendedContext() throws Exception {
+ JsonObject status = buildContextStatus("suspendedApp", 7);
+ writeStatusFile(TEST_PID, status);
+
+ CamelContextStatus command = new CamelContextStatus(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertTrue(printer.getOutput().contains("Suspended"), "Phase 7 should show Suspended");
+ }
+ }
+
+ @Test
+ void testRouteCountsInOutput() throws Exception {
+ JsonObject status = buildContextStatus("routeCountApp", 5);
+ // Add two routes: one started, one suspended
+ JsonArray routes = new JsonArray();
+ JsonObject r1 = new JsonObject();
+ r1.put("state", "Started");
+ JsonObject r2 = new JsonObject();
+ r2.put("state", "Suspended");
+ routes.add(r1);
+ routes.add(r2);
+ status.put("routes", routes);
+ writeStatusFile(TEST_PID, status);
+
+ CamelContextStatus command = new CamelContextStatus(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ // The context status shows started/total routes — 1 started out of 2 total
+ String output = printer.getOutput();
+ assertTrue(output.contains("1/2"), "Should show 1 started out of 2 total routes");
+ }
+ }
+
+ @Test
+ void testEmptyOutputWhenNoProcesses() throws Exception {
+ CamelContextStatus command = new CamelContextStatus(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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("jsonApp", 5);
+ writeStatusFile(TEST_PID, status);
+
+ CamelContextStatus command = new CamelContextStatus(new CamelJBangMain().withPrinter(printer));
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 a JSON array");
+ assertTrue(output.contains("jsonApp"), "JSON should contain the context name");
+ }
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
new file mode 100644
index 000000000000..68261a1f36ce
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/CamelRouteStatusTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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 CamelRouteStatusTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoProcesses() throws Exception {
+ CamelRouteStatus command = new CamelRouteStatus(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.empty());
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testDisplaysSingleRoute() throws Exception {
+ JsonObject status = buildRouteStatus("route1", "Started");
+ writeStatusFile(TEST_PID, status);
+
+ CamelRouteStatus command = new CamelRouteStatus(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("route1"), "Should show route ID");
+ assertTrue(output.contains("timer:tick"), "Should show from URI");
+ assertTrue(output.contains("Started"), "Should show route state");
+ }
+ }
+
+ @Test
+ void testDisplaysMultipleRoutes() throws Exception {
+ JsonObject routeStats = buildRouteStats();
+ JsonObject route1 = buildRoute("route1", "timer:tick1", routeStats);
+ JsonObject route2 = buildRoute("route2", "timer:tick2", routeStats);
+
+ JsonArray routes = new JsonArray();
+ routes.add(route1);
+ routes.add(route2);
+
+ JsonObject context = new JsonObject();
+ context.put("name", "multiRouteApp");
+ context.put("phase", 5);
+
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ root.put("routes", routes);
+ writeStatusFile(TEST_PID, root);
+
+ CamelRouteStatus command = new CamelRouteStatus(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("route1"));
+ assertTrue(output.contains("route2"));
+ }
+ }
+
+ @Test
+ void testSuspendedRouteShownCorrectly() throws Exception {
+ JsonObject status = buildRouteStatus("suspendedRoute", "Suspended");
+ writeStatusFile(TEST_PID, status);
+
+ CamelRouteStatus command = new CamelRouteStatus(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertTrue(printer.getOutput().contains("Suspended"));
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject status = buildRouteStatus("route1", "Started");
+ writeStatusFile(TEST_PID, status);
+
+ CamelRouteStatus command = new CamelRouteStatus(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("route1"));
+ }
+ }
+
+ @Test
+ void testFilterByRouteId() throws Exception {
+ JsonObject routeStats = buildRouteStats();
+ JsonObject route1 = buildRoute("matchingRoute", "timer:tick1", routeStats);
+ JsonObject route2 = buildRoute("otherRoute", "timer:tick2", routeStats);
+
+ JsonArray routes = new JsonArray();
+ routes.add(route1);
+ routes.add(route2);
+
+ JsonObject context = new JsonObject();
+ context.put("name", "myApp");
+ context.put("phase", 5);
+
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ root.put("routes", routes);
+ writeStatusFile(TEST_PID, root);
+
+ CamelRouteStatus command = new CamelRouteStatus(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.filter = new String[] { "matchingRoute" };
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("matchingRoute"), "Filtered route should appear");
+ assertFalse(output.contains("otherRoute"), "Non-matching route should be hidden by filter");
+ }
+ }
+
+ @Test
+ void testLimitReducesRows() throws Exception {
+ JsonObject routeStats = buildRouteStats();
+ JsonArray routes = new JsonArray();
+ for (int i = 1; i <= 5; i++) {
+ routes.add(buildRoute("route" + i, "timer:tick" + i, routeStats));
+ }
+
+ JsonObject context = new JsonObject();
+ context.put("name", "bigApp");
+ context.put("phase", 5);
+
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ root.put("routes", routes);
+ writeStatusFile(TEST_PID, root);
+
+ CamelRouteStatus command = new CamelRouteStatus(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.limit = 2;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ // Only 2 routes should appear — limit is enforced during row collection
+ assertTrue(output.contains("route1"));
+ assertTrue(output.contains("route2"));
+ assertFalse(output.contains("route5"), "route5 should be excluded by limit=2");
+ }
+ }
+
+ private static JsonObject buildRouteStats() {
+ JsonObject stats = new JsonObject();
+ stats.put("exchangesTotal", "0");
+ stats.put("exchangesFailed", "0");
+ stats.put("exchangesInflight", "0");
+ stats.put("meanProcessingTime", "-1");
+ stats.put("maxProcessingTime", "0");
+ stats.put("minProcessingTime", "0");
+ return stats;
+ }
+
+ private static JsonObject buildRoute(String routeId, String fromUri, JsonObject stats) {
+ JsonObject route = new JsonObject();
+ route.put("routeId", routeId);
+ route.put("from", fromUri);
+ route.put("state", "Started");
+ route.put("uptime", "1m");
+ 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/ListBlockedTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListBlockedTest.java
new file mode 100644
index 000000000000..25f247ca37e7
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListBlockedTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.Collections;
+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 ListBlockedTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoBlockedExchanges() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListBlocked command = new ListBlocked(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsBlockedExchange() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("blocked", exchangeContainer(blockedExchange("exchange-1", "route1", "to1")));
+ writeStatusFile(TEST_PID, root);
+
+ ListBlocked command = new ListBlocked(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("exchange-1"), "Should show blocked exchange id");
+ assertTrue(output.contains("route1"), "Should show route id");
+ assertTrue(output.contains("to1"), "Should show node id");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("blocked", exchangeContainer(blockedExchange("exchange-2", "route2", "bean1")));
+ writeStatusFile(TEST_PID, root);
+
+ ListBlocked command = new ListBlocked(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("exchange-2"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject exchangeContainer(JsonObject... exchanges) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, exchanges);
+ JsonObject container = new JsonObject();
+ container.put("exchanges", arr);
+ return container;
+ }
+
+ private static JsonObject blockedExchange(String exchangeId, String routeId, String nodeId) {
+ JsonObject e = new JsonObject();
+ e.put("exchangeId", exchangeId);
+ e.put("routeId", routeId);
+ e.put("nodeId", nodeId);
+ e.put("duration", 1500L);
+ return e;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListCircuitBreakerTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListCircuitBreakerTest.java
new file mode 100644
index 000000000000..32c73e1dc511
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListCircuitBreakerTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.Collections;
+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 ListCircuitBreakerTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoCircuitBreakers() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListCircuitBreaker command = new ListCircuitBreaker(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsClosedCircuitBreaker() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("resilience4j", cbContainer(cbEntry("CLOSED")));
+ writeStatusFile(TEST_PID, root);
+
+ ListCircuitBreaker command = new ListCircuitBreaker(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("myCB"), "Should show circuit breaker ID");
+ assertTrue(output.contains("CLOSED"), "Should show CLOSED state");
+ }
+ }
+
+ @Test
+ void testShowsOpenCircuitBreaker() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("resilience4j", cbContainer(cbEntry("OPEN")));
+ writeStatusFile(TEST_PID, root);
+
+ ListCircuitBreaker command = new ListCircuitBreaker(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertTrue(printer.getOutput().contains("OPEN"), "Should show OPEN state");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("resilience4j", cbContainer(cbEntry("CLOSED")));
+ writeStatusFile(TEST_PID, root);
+
+ ListCircuitBreaker command = new ListCircuitBreaker(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("myCB"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject cbContainer(JsonObject... cbs) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, cbs);
+ JsonObject container = new JsonObject();
+ container.put("circuitBreakers", arr);
+ return container;
+ }
+
+ private static JsonObject cbEntry(String state) {
+ JsonObject cb = new JsonObject();
+ cb.put("id", "myCB");
+ cb.put("routeId", "myRoute");
+ cb.put("state", state);
+ cb.put("bufferedCalls", 0);
+ cb.put("successfulCalls", 0);
+ cb.put("failedCalls", 0);
+ cb.put("notPermittedCalls", 0L);
+ cb.put("failureRate", 0.0);
+ return cb;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListConsumerTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListConsumerTest.java
new file mode 100644
index 000000000000..d31e51f557bd
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListConsumerTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.Collections;
+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 ListConsumerTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoConsumers() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListConsumer command = new ListConsumer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsConsumerUriAndState() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("consumers", consumerContainer(consumer()));
+ writeStatusFile(TEST_PID, root);
+
+ ListConsumer command = new ListConsumer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("timer:tick"), "Should show consumer URI");
+ assertTrue(output.contains("Started"), "Should show consumer state");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("consumers", consumerContainer(consumer()));
+ writeStatusFile(TEST_PID, root);
+
+ ListConsumer command = new ListConsumer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("timer:tick"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject consumerContainer(JsonObject... consumers) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, consumers);
+ JsonObject container = new JsonObject();
+ container.put("consumers", arr);
+ return container;
+ }
+
+ private static JsonObject consumer() {
+ JsonObject c = new JsonObject();
+ c.put("id", "route1");
+ c.put("uri", "timer:tick");
+ c.put("state", "Started");
+ c.put("class", "org.apache.camel.component.timer.TimerConsumer");
+ c.put("inflight", 0);
+ c.put("polling", false);
+ c.put("scheduled", false);
+ c.put("hostedService", false);
+ c.put("totalCounter", 0L);
+ c.put("delay", 0L);
+ c.put("period", 1000L);
+ return c;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java
new file mode 100644
index 000000000000..e2605a1340f3
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpointTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.Collections;
+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 ListEndpointTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testDisplaysEndpoints() throws Exception {
+ JsonObject root = buildEndpointStatus("myApp",
+ endpoint("timer:tick", "in", "10"),
+ endpoint("log:info", "out", "10"));
+ writeStatusFile(TEST_PID, root);
+
+ ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("timer:tick"), "Should show consumer endpoint");
+ assertTrue(output.contains("log:info"), "Should show producer endpoint");
+ }
+ }
+
+ @Test
+ void testEmptyOutputWhenNoEndpoints() throws Exception {
+ JsonObject root = buildEndpointStatus("myApp");
+ writeStatusFile(TEST_PID, root);
+
+ ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testFilterByDirection() throws Exception {
+ JsonObject root = buildEndpointStatus("myApp",
+ endpoint("timer:tick", "in", "5"),
+ endpoint("kafka:topic", "out", "5"));
+ writeStatusFile(TEST_PID, root);
+
+ ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.filterDirection = "in";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("timer:tick"), "Consumer endpoint should be shown for direction=in");
+ assertFalse(output.contains("kafka:topic"), "Producer endpoint should be hidden for direction=in");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = buildEndpointStatus("myApp",
+ endpoint("timer:tick", "in", "1"));
+ writeStatusFile(TEST_PID, root);
+
+ ListEndpoint command = new ListEndpoint(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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("timer:tick"));
+ }
+ }
+
+ private static JsonObject buildEndpointStatus(String contextName, JsonObject... endpoints) {
+ JsonArray epArr = new JsonArray();
+ Collections.addAll(epArr, endpoints);
+
+ JsonObject epContainer = new JsonObject();
+ epContainer.put("endpoints", epArr);
+
+ JsonObject context = new JsonObject();
+ context.put("name", contextName);
+
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ root.put("endpoints", epContainer);
+ return root;
+ }
+
+ private static JsonObject endpoint(String uri, String direction, String hits) {
+ JsonObject ep = new JsonObject();
+ ep.put("uri", uri);
+ ep.put("direction", direction);
+ ep.put("hits", hits);
+ ep.put("stub", false);
+ ep.put("remote", true);
+ return ep;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListErrorTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListErrorTest.java
new file mode 100644
index 000000000000..c17661f43cb5
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListErrorTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.Collections;
+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.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 ListErrorTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoErrors() throws Exception {
+ JsonObject status = buildContextStatus("myApp", 5);
+ writeStatusFile(TEST_PID, status);
+ // no error file written
+
+ ListError command = new ListError(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsErrorRouteId() throws Exception {
+ JsonObject status = buildContextStatus("myApp", 5);
+ writeStatusFile(TEST_PID, status);
+ writeErrorFile(errorEntry());
+
+ ListError command = new ListError(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("myRoute"), "Should show routeId");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject status = buildContextStatus("myApp", 5);
+ writeStatusFile(TEST_PID, status);
+ writeErrorFile(errorEntry());
+
+ ListError command = new ListError(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("myRoute"));
+ }
+ }
+
+ private static void writeErrorFile(JsonObject... errors) throws Exception {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, errors);
+ JsonObject root = new JsonObject();
+ root.put("errors", arr);
+ Path f = CommandLineHelper.getCamelDir().resolve(ProcessCommandTestSupport.TEST_PID + "-error.json");
+ Files.writeString(f, root.toJson());
+ }
+
+ private static JsonObject errorEntry() {
+ JsonObject ex = new JsonObject();
+ ex.put("type", "java.lang.RuntimeException");
+ ex.put("message", "Test error");
+
+ JsonObject e = new JsonObject();
+ e.put("routeId", "myRoute");
+ e.put("nodeId", "myNode");
+ e.put("exchangeId", "abc-123");
+ e.put("handled", false);
+ e.put("timestamp", System.currentTimeMillis());
+ e.put("exception", ex);
+ return e;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEventTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEventTest.java
new file mode 100644
index 000000000000..16ceaaac64e7
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListEventTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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.Collections;
+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 ListEventTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoEvents() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListEvent command = new ListEvent(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsAllEventGroups() throws Exception {
+ JsonObject root = buildEventStatus();
+ writeStatusFile(TEST_PID, root);
+
+ ListEvent command = new ListEvent(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("context started"), "Should show context events");
+ assertTrue(output.contains("route started"), "Should show route events");
+ assertTrue(output.contains("exchange done"), "Should show exchange events");
+ }
+ }
+
+ @Test
+ void testFilterRouteEvents() throws Exception {
+ JsonObject root = buildEventStatus();
+ writeStatusFile(TEST_PID, root);
+
+ ListEvent command = new ListEvent(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.filter = "route";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("route started"), "Route events should be shown");
+ assertFalse(output.contains("context started"), "Context events should be filtered out");
+ assertFalse(output.contains("exchange done"), "Exchange events should be filtered out");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = buildEventStatus();
+ writeStatusFile(TEST_PID, root);
+
+ ListEvent command = new ListEvent(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("ContextStarted"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject buildEventStatus() {
+ JsonObject events = new JsonObject();
+ events.put("events", eventArray(event("ContextStarted", "context started")));
+ events.put("routeEvents", eventArray(event("RouteStarted", "route started")));
+ events.put("exchangeEvents", eventArray(event("ExchangeCompleted", "exchange done")));
+
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("events", events);
+ return root;
+ }
+
+ private static JsonArray eventArray(JsonObject... events) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, events);
+ return arr;
+ }
+
+ private static JsonObject event(String type, String message) {
+ JsonObject e = new JsonObject();
+ e.put("type", type);
+ e.put("timestamp", System.currentTimeMillis() - 1000);
+ e.put("exchangeId", "exchange-1");
+ e.put("message", message);
+ return e;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealthTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealthTest.java
new file mode 100644
index 000000000000..a386b9b6cce3
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListHealthTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.Collections;
+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 ListHealthTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoProcesses() throws Exception {
+ ListHealth command = new ListHealth(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.empty());
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsUpHealthCheck() throws Exception {
+ JsonObject root = buildHealthStatus(check("context", "UP", true));
+ writeStatusFile(TEST_PID, root);
+
+ ListHealth command = new ListHealth(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("UP"), "Should show UP state");
+ assertTrue(output.contains("context"), "Should show check ID");
+ }
+ }
+
+ @Test
+ void testShowsDownHealthCheck() throws Exception {
+ JsonObject root = buildHealthStatus(check("context", "DOWN", true));
+ writeStatusFile(TEST_PID, root);
+
+ ListHealth command = new ListHealth(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertTrue(printer.getOutput().contains("DOWN"), "Should show DOWN state");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = buildHealthStatus(check("context", "UP", true));
+ writeStatusFile(TEST_PID, root);
+
+ ListHealth command = new ListHealth(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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("context"));
+ }
+ }
+
+ @Test
+ void testFilterReadyOnly() throws Exception {
+ JsonObject root = buildHealthStatus(
+ check("context", "UP", true),
+ check("consumer", "DOWN", false));
+ writeStatusFile(TEST_PID, root);
+
+ ListHealth command = new ListHealth(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.ready = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("context"), "Readiness check should be shown");
+ assertFalse(output.contains("consumer"), "Non-readiness check should be hidden with --ready");
+ }
+ }
+
+ private static JsonObject buildHealthStatus(JsonObject... checks) {
+ JsonArray checksArr = new JsonArray();
+ Collections.addAll(checksArr, checks);
+
+ JsonObject hc = new JsonObject();
+ hc.put("checks", checksArr);
+
+ JsonObject context = new JsonObject();
+ context.put("name", "myApp");
+
+ JsonObject root = new JsonObject();
+ root.put("context", context);
+ root.put("healthChecks", hc);
+ return root;
+ }
+
+ private static JsonObject check(String id, String state, boolean readiness) {
+ JsonObject c = new JsonObject();
+ c.put("id", id);
+ c.put("group", "camel");
+ c.put("state", state);
+ c.put("readiness", readiness);
+ c.put("liveness", true);
+ return c;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListInflightTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListInflightTest.java
new file mode 100644
index 000000000000..6caec235e053
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListInflightTest.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.Collections;
+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 ListInflightTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoInflightExchanges() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListInflight command = new ListInflight(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsInflightExchangeWithRemoteColumn() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("inflight", exchangeContainer(inflightExchange("exchange-1", true)));
+ writeStatusFile(TEST_PID, root);
+
+ ListInflight command = new ListInflight(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("exchange-1"), "Should show inflight exchange id");
+ assertTrue(output.contains("REMOTE"), "Remote column should be visible when status has remote data");
+ assertTrue(output.contains("to1"), "Should show current node id");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("inflight", exchangeContainer(inflightExchange("exchange-2", false)));
+ writeStatusFile(TEST_PID, root);
+
+ ListInflight command = new ListInflight(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("\"remote\":false"));
+ assertTrue(output.contains("exchange-2"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject exchangeContainer(JsonObject... exchanges) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, exchanges);
+ JsonObject container = new JsonObject();
+ container.put("exchanges", arr);
+ return container;
+ }
+
+ private static JsonObject inflightExchange(String exchangeId, boolean remote) {
+ JsonObject e = new JsonObject();
+ e.put("exchangeId", exchangeId);
+ e.put("fromRouteId", "route1");
+ e.put("fromRemoteEndpoint", remote);
+ e.put("atRouteId", "route1");
+ e.put("nodeId", "to1");
+ e.put("elapsed", 100L);
+ e.put("duration", 250L);
+ return e;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTaskTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTaskTest.java
new file mode 100644
index 000000000000..7b700e6ffc7d
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListInternalTaskTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.Collections;
+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 ListInternalTaskTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoInternalTasks() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListInternalTask command = new ListInternalTask(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsInternalTask() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("internal-tasks", taskContainer(task("reload", "Blocked")));
+ writeStatusFile(TEST_PID, root);
+
+ ListInternalTask command = new ListInternalTask(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("reload"), "Should show task name");
+ assertTrue(output.contains("Blocked"), "Should show task status");
+ assertTrue(output.contains("boom"), "Should show task failure");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("internal-tasks", taskContainer(task("sync", "Done")));
+ writeStatusFile(TEST_PID, root);
+
+ ListInternalTask command = new ListInternalTask(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("sync"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject taskContainer(JsonObject... tasks) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, tasks);
+ JsonObject container = new JsonObject();
+ container.put("tasks", arr);
+ return container;
+ }
+
+ private static JsonObject task(String name, String status) {
+ long now = System.currentTimeMillis();
+ JsonObject t = new JsonObject();
+ t.put("name", name);
+ t.put("status", status);
+ t.put("attempts", 2L);
+ t.put("delay", 1000L);
+ t.put("elapsed", 50L);
+ t.put("firstTime", now - 5000);
+ t.put("lastTime", now - 1000);
+ t.put("nextTime", now + 1000);
+ t.put("error", "boom");
+ return t;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetricTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetricTest.java
new file mode 100644
index 000000000000..40f6909a0a8a
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListMetricTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.Collections;
+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 ListMetricTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoMetrics() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListMetric command = new ListMetric(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsCounterMetric() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("micrometer", micrometerObj(counterEntry(42.0)));
+ writeStatusFile(TEST_PID, root);
+
+ ListMetric command = new ListMetric(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.all = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("myCounter"), "Should show metric name");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("micrometer", micrometerObj(counterEntry(10.0)));
+ writeStatusFile(TEST_PID, root);
+
+ ListMetric command = new ListMetric(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.all = true;
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("myCounter"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject micrometerObj(JsonObject... counters) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, counters);
+ JsonObject mo = new JsonObject();
+ mo.put("counters", arr);
+ return mo;
+ }
+
+ private static JsonObject counterEntry(double count) {
+ JsonObject c = new JsonObject();
+ c.put("name", "myCounter");
+ c.put("description", "");
+ c.put("count", count);
+ return c;
+ }
+}
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
new file mode 100644
index 000000000000..4c277d82bee5
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProcessTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.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 ListProcessTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoProcesses() throws Exception {
+ ListProcess command = new ListProcess(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.empty());
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testListsRunningProcess() throws Exception {
+ JsonObject stats = buildStats();
+ JsonObject status = buildContextStatus("myApp", 5);
+ ((JsonObject) status.get("context")).put("statistics", stats);
+ writeStatusFile(TEST_PID, status);
+
+ ListProcess command = new ListProcess(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 contain context name");
+ assertTrue(output.contains("Running"), "Should show Running status for phase 5");
+ assertTrue(output.contains("1/1"), "Should show ready 1/1 when phase is 5");
+ }
+ }
+
+ @Test
+ void testListsStartingProcess() throws Exception {
+ // Phase 3 = Starting (anything <= 4 is Starting)
+ JsonObject status = buildContextStatus("startingApp", 3);
+ writeStatusFile(TEST_PID, status);
+
+ ListProcess command = new ListProcess(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("startingApp"));
+ assertTrue(output.contains("Starting"), "Phase <= 4 should map to Starting");
+ assertTrue(output.contains("0/1"), "Should show 0/1 ready when not phase 5");
+ }
+ }
+
+ @Test
+ void testPidOnlyFlag() throws Exception {
+ JsonObject status = buildContextStatus("myApp", 5);
+ writeStatusFile(TEST_PID, status);
+
+ ListProcess command = new ListProcess(new CamelJBangMain().withPrinter(printer));
+ command.pid = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ List<String> lines = printer.getLines();
+ assertEquals(1, lines.size(), "Should output exactly one PID line");
+ assertEquals(String.valueOf(TEST_PID), lines.get(0).trim());
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject status = buildContextStatus("myApp", 5);
+ writeStatusFile(TEST_PID, status);
+
+ ListProcess command = new ListProcess(new CamelJBangMain().withPrinter(printer));
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 a JSON array");
+ assertTrue(output.contains("myApp"), "JSON should contain the context name");
+ }
+ }
+
+ @Test
+ void testJsonPidOnlyFlag() throws Exception {
+ JsonObject status = buildContextStatus("myApp", 5);
+ writeStatusFile(TEST_PID, status);
+
+ ListProcess command = new ListProcess(new CamelJBangMain().withPrinter(printer));
+ command.pid = true;
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.startsWith("["), "JSON pid output should be a JSON array");
+ assertTrue(output.contains(String.valueOf(TEST_PID)), "Should contain the PID");
+ }
+ }
+
+ @Test
+ void testSortByName() throws Exception {
+ long pid1 = 11111L;
+ long pid2 = 22222L;
+
+ JsonObject status1 = buildContextStatus("zebra", 5);
+ JsonObject status2 = buildContextStatus("apple", 5);
+ writeStatusFile(pid1, status1);
+ writeStatusFile(pid2, status2);
+
+ 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 currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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");
+ }
+ }
+
+ @Test
+ void testReloadErrorShownInOutput() throws Exception {
+ JsonObject status = buildContextStatus("faultyApp", 5);
+ JsonObject stats = (JsonObject) ((JsonObject) status.get("context")).get("statistics");
+ JsonObject reload = new JsonObject();
+ JsonObject lastError = new JsonObject();
+ lastError.put("message", "Route file parse error");
+ reload.put("lastError", lastError);
+ stats.put("reload", reload);
+ writeStatusFile(TEST_PID, status);
+
+ ListProcess command = new ListProcess(new CamelJBangMain().withPrinter(printer));
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("Error"), "Should show Error status when reload failed");
+ // Description column is width-limited; "Reload" is the start of the description
+ assertTrue(output.contains("Reload"), "Should display the reload description");
+ }
+ }
+
+ private static JsonObject buildStats() {
+ JsonObject stats = new JsonObject();
+ stats.put("exchangesTotal", "100");
+ stats.put("exchangesFailed", "5");
+ stats.put("exchangesInflight", "3");
+ return stats;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProducerTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProducerTest.java
new file mode 100644
index 000000000000..90d58230e34d
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListProducerTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.Collections;
+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 ListProducerTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoProducers() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListProducer command = new ListProducer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsProducerUriAndState() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("producers", producerContainer(producer("route1", "log:info?showAll=true")));
+ writeStatusFile(TEST_PID, root);
+
+ ListProducer command = new ListProducer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("log:info"), "Should show producer URI");
+ assertTrue(output.contains("Started"), "Should show producer state");
+ }
+ }
+
+ @Test
+ void testFilterAndShortUri() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("producers", producerContainer(
+ producer("route1", "log:info?showAll=true"),
+ producer("route2", "mock:result?retainFirst=1")));
+ writeStatusFile(TEST_PID, root);
+
+ ListProducer command = new ListProducer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.filter = "log:";
+ command.shortUri = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("log:info"), "Matching producer should be shown");
+ assertFalse(output.contains("showAll"), "Short URI should remove query parameters");
+ assertFalse(output.contains("mock:result"), "Non-matching producer should be hidden");
+ }
+ }
+
+ @Test
+ void testLimit() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("producers", producerContainer(
+ producer("route1", "log:one"),
+ producer("route2", "log:two")));
+ writeStatusFile(TEST_PID, root);
+
+ ListProducer command = new ListProducer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.limit = 1;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("log:one"), "First producer should be shown");
+ assertFalse(output.contains("log:two"), "Second producer should be hidden by limit");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("producers", producerContainer(producer("route1", "direct:out")));
+ writeStatusFile(TEST_PID, root);
+
+ ListProducer command = new ListProducer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("direct:out"));
+ assertTrue(output.contains("Log"), "Producer suffix should be removed from type");
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject producerContainer(JsonObject... producers) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, producers);
+ JsonObject container = new JsonObject();
+ container.put("producers", arr);
+ return container;
+ }
+
+ private static JsonObject producer(String routeId, String uri) {
+ JsonObject p = new JsonObject();
+ p.put("routeId", routeId);
+ p.put("uri", uri);
+ p.put("state", "Started");
+ p.put("class", "org.apache.camel.component.log.LogProducer");
+ p.put("singleton", true);
+ p.put("remote", false);
+ return p;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListPropertiesTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListPropertiesTest.java
new file mode 100644
index 000000000000..8ab0232b492f
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListPropertiesTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.Collections;
+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 ListPropertiesTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoProperties() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("properties", propertyContainer("properties"));
+ writeStatusFile(TEST_PID, root);
+
+ ListProperties command = new ListProperties(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testMasksSensitiveValuesByDefault() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("properties", propertyContainer("properties",
+ property("camel.password", "secret", "secret", false, "properties:app", "initial")));
+ writeStatusFile(TEST_PID, root);
+
+ ListProperties command = new ListProperties(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ // "camel.password" is 14 chars; the KEY column minimum is 15 but may truncate with ellipsis
+ // in narrow terminal environments — check the prefix that always survives truncation
+ assertTrue(output.contains("camel.pass"), "Should show sensitive property key (possibly truncated)");
+ assertTrue(output.contains("xxxxxx"), "Sensitive value should be masked");
+ assertFalse(output.contains("secret"), "Raw sensitive value should not be printed");
+ }
+ }
+
+ @Test
+ void testInternalPropertiesHiddenUnlessRequested() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("properties", propertyContainer("properties",
+ property("public.key", "visible", "visible", false, "properties:app", "ENV"),
+ property("internal.key", "hidden", "hidden", true, "properties:app", "SYS")));
+ writeStatusFile(TEST_PID, root);
+
+ ListProperties command = new ListProperties(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("public.key"), "Public property should be shown");
+ assertFalse(output.contains("internal.key"), "Internal property should be hidden by default");
+ }
+ }
+
+ @Test
+ void testStartupVerboseJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("main-configuration", propertyContainer("configurations",
+ property("camel.main.name", "demo", "demo", false, "sys:camel.main.name", "CLI")));
+ writeStatusFile(TEST_PID, root);
+
+ ListProperties command = new ListProperties(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.startup = true;
+ command.verbose = true;
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("camel.main.name"));
+ assertTrue(output.contains("Command Line"), "CLI location should be sanitized");
+ assertTrue(output.contains("\"function\":\"sys\""), "Function should be derived from source prefix");
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject propertyContainer(String key, JsonObject... properties) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, properties);
+ JsonObject container = new JsonObject();
+ container.put(key, arr);
+ return container;
+ }
+
+ private static JsonObject property(
+ String key, String value, String originalValue, boolean internal, String source, String location) {
+ JsonObject p = new JsonObject();
+ p.put("key", key);
+ p.put("value", value);
+ p.put("originalValue", originalValue);
+ p.put("internal", internal);
+ p.put("source", source);
+ p.put("location", location);
+ return p;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListRestTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListRestTest.java
new file mode 100644
index 000000000000..2672932e43a6
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListRestTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.Collections;
+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 ListRestTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoRests() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListRest command = new ListRest(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsRestEndpoints() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("rests", restContainer(restEntry("/api/users", "GET")));
+ writeStatusFile(TEST_PID, root);
+
+ ListRest command = new ListRest(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("api/users"), "Should show URL");
+ assertTrue(output.contains("GET"), "Should show HTTP method");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("rests", restContainer(restEntry("/api/items", "POST")));
+ writeStatusFile(TEST_PID, root);
+
+ ListRest command = new ListRest(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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("items"), "Should contain the REST URL path");
+ assertTrue(output.contains("POST"), "Should contain the HTTP method");
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject restContainer(JsonObject... entries) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, entries);
+ JsonObject container = new JsonObject();
+ container.put("rests", arr);
+ return container;
+ }
+
+ private static JsonObject restEntry(String url, String method) {
+ JsonObject e = new JsonObject();
+ e.put("url", url);
+ e.put("method", method);
+ e.put("consumes", "application/json");
+ e.put("produces", "application/json");
+ e.put("description", "");
+ e.put("contractFirst", false);
+ return e;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListServiceTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListServiceTest.java
new file mode 100644
index 000000000000..315902e39040
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListServiceTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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.Collections;
+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 ListServiceTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoServices() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListService command = new ListService(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsServiceDetails() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("services", serviceContainer(service("platform-http", "in", "http", "http://0.0.0.0:8080/hello")));
+ writeStatusFile(TEST_PID, root);
+
+ ListService command = new ListService(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("platform-http"), "Should show service component");
+ assertTrue(output.contains("http://0.0.0.0:8080/hello"), "Should show service URL");
+ }
+ }
+
+ @Test
+ void testMetadataAndShortUri() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("services", serviceContainer(service("rest", "in", "http", "http://localhost:8080/orders")));
+ writeStatusFile(TEST_PID, root);
+
+ ListService command = new ListService(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.metadata = true;
+ command.shortUri = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("method=GET"), "Metadata should be shown when requested");
+ assertTrue(output.contains("direct:orders"), "Endpoint URI should be shown");
+ assertFalse(output.contains("bridgeEndpoint"), "Short URI should remove endpoint query parameters");
+ }
+ }
+
+ @Test
+ void testJsonOutputUsesEmptyRouteWhenMissing() throws Exception {
+ JsonObject entry = service("servlet", "in", "http", "http://localhost:8080/ping");
+ entry.remove("routeId");
+
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("services", serviceContainer(entry));
+ writeStatusFile(TEST_PID, root);
+
+ ListService command = new ListService(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("\"routeId\":\"\""), "Missing route id should be serialized as an empty string");
+ assertTrue(output.contains("servlet"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject serviceContainer(JsonObject... services) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, services);
+ JsonObject container = new JsonObject();
+ container.put("services", arr);
+ return container;
+ }
+
+ private static JsonObject service(String component, String direction, String protocol, String serviceUrl) {
+ JsonObject metadata = new JsonObject();
+ metadata.put("method", "GET");
+ metadata.put("path", "/orders");
+
+ JsonObject s = new JsonObject();
+ s.put("component", component);
+ s.put("direction", direction);
+ s.put("hosted", true);
+ s.put("protocol", protocol);
+ s.put("serviceUrl", serviceUrl);
+ s.put("endpointUri", "direct:orders?bridgeEndpoint=true");
+ s.put("hits", 3L);
+ s.put("routeId", "route1");
+ s.put("metadata", metadata);
+ return s;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListTransformerTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListTransformerTest.java
new file mode 100644
index 000000000000..4deeff87521e
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListTransformerTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.Collections;
+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 ListTransformerTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoTransformers() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListTransformer command = new ListTransformer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsTransformerDataTypes() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("transformers", transformerContainer(transformer("order-transformer", "xml:Order", "json:Order")));
+ writeStatusFile(TEST_PID, root);
+
+ ListTransformer command = new ListTransformer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("order-transformer"), "Should show transformer name");
+ assertTrue(output.contains("xml:Order"), "Should show source data type");
+ assertTrue(output.contains("json:Order"), "Should show target data type");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("transformers", transformerContainer(transformer("order-transformer", "xml:Order", "json:Order")));
+ writeStatusFile(TEST_PID, root);
+
+ ListTransformer command = new ListTransformer(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("order-transformer"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject transformerContainer(JsonObject... transformers) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, transformers);
+ JsonObject container = new JsonObject();
+ container.put("transformers", arr);
+ return container;
+ }
+
+ private static JsonObject transformer(String name, String from, String to) {
+ JsonObject t = new JsonObject();
+ t.put("name", name);
+ t.put("from", from);
+ t.put("to", to);
+ return t;
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListVariableTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListVariableTest.java
new file mode 100644
index 000000000000..cb8bd15fff39
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListVariableTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.Collections;
+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 ListVariableTest extends ProcessCommandTestSupport {
+
+ @Test
+ void testEmptyOutputWhenNoVariables() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ writeStatusFile(TEST_PID, root);
+
+ ListVariable command = new ListVariable(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ assertEquals("", printer.getOutput().trim());
+ }
+ }
+
+ @Test
+ void testShowsVariableKeyAndValue() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("variables", variablesObj(variableEntry("myKey", "String", "hello")));
+ writeStatusFile(TEST_PID, root);
+
+ ListVariable command = new ListVariable(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+
+ int exit = command.doCall();
+
+ assertEquals(0, exit);
+ String output = printer.getOutput();
+ assertTrue(output.contains("myKey"), "Should show variable key");
+ assertTrue(output.contains("hello"), "Should show variable value");
+ }
+ }
+
+ @Test
+ void testJsonOutput() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("context", contextObj());
+ root.put("variables", variablesObj(variableEntry("counter", "Integer", "42")));
+ writeStatusFile(TEST_PID, root);
+
+ ListVariable command = new ListVariable(new CamelJBangMain().withPrinter(printer));
+ command.sort = "pid";
+ command.jsonOutput = true;
+
+ try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+ ProcessHandle ph = mockProcessHandle(TEST_PID);
+ ProcessHandle currentHandle = mockCurrentHandle();
+ mocked.when(ProcessHandle::current).thenReturn(currentHandle);
+ 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 array");
+ assertTrue(output.contains("counter"));
+ }
+ }
+
+ private static JsonObject contextObj() {
+ JsonObject ctx = new JsonObject();
+ ctx.put("name", "myApp");
+ return ctx;
+ }
+
+ private static JsonObject variablesObj(JsonObject... entries) {
+ JsonArray arr = new JsonArray();
+ Collections.addAll(arr, entries);
+ JsonObject vars = new JsonObject();
+ vars.put("global", arr);
+ return vars;
+ }
+
+ private static JsonObject variableEntry(String key, String type, String value) {
+ JsonObject e = new JsonObject();
+ e.put("key", key);
+ e.put("type", type);
+ e.put("value", value);
+ return e;
+ }
+}