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/22 20:49:59 UTC

(camel) branch main updated: CAMEL-23688: camel-jbang - add unit tests for action-writer 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 459355c5865e CAMEL-23688: camel-jbang - add unit tests for action-writer CLI commands
459355c5865e is described below

commit 459355c5865e1bfa2c91ee069c7ab62d37815e11
Author: Adriano Machado <60...@users.noreply.github.com>
AuthorDate: Mon Jun 22 16:49:53 2026 -0400

    CAMEL-23688: camel-jbang - add unit tests for action-writer CLI commands
    
    Adds unit test coverage for the fire-and-forget action commands in
    camel-jbang-core: start/stop/suspend/resume-route, start/stop-group,
    reload, reset-stats, enable/disable-processor. Includes a shared
    ActionCommandTestSupport base and tests that verify exact JSON action
    payloads and no-match behavior.
    
    Closes #24188
    
    Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
---
 .../commands/action/ActionCommandTestSupport.java  | 144 +++++++++++++++++++++
 .../action/CamelProcessorDisableActionTest.java    |  62 +++++++++
 .../action/CamelProcessorEnableActionTest.java     |  62 +++++++++
 .../commands/action/CamelReloadActionTest.java     |  59 +++++++++
 .../commands/action/CamelResetStatsActionTest.java |  59 +++++++++
 .../action/CamelRouteGroupStartActionTest.java     |  64 +++++++++
 .../action/CamelRouteGroupStopActionTest.java      |  64 +++++++++
 .../action/CamelRouteResumeActionTest.java         |  62 +++++++++
 .../commands/action/CamelRouteStartActionTest.java |  78 +++++++++++
 .../commands/action/CamelRouteStopActionTest.java  |  62 +++++++++
 .../action/CamelRouteSuspendActionTest.java        |  62 +++++++++
 11 files changed, 778 insertions(+)

diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/ActionCommandTestSupport.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/ActionCommandTestSupport.java
new file mode 100644
index 000000000000..9a31d872dd30
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/ActionCommandTestSupport.java
@@ -0,0 +1,144 @@
+/*
+ * 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.Comparator;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.MockedStatic;
+
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Base class for action command tests. Handles file system setup, status file writing and ProcessHandle mock creation
+ * for use with Mockito's mockStatic. Mirrors the proven {@code ProcessCommandTestSupport} (in the {@code process} test
+ * package) and adds helpers for the action/output file round trip that action commands use.
+ */
+abstract class ActionCommandTestSupport extends CamelCommandBaseTestSupport {
+
+    static final long TEST_PID = 12345L;
+    static final long CURRENT_PID = 99999L;
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        super.setup();
+        CommandLineHelper.useHomeDir("target/test-action");
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+    }
+
+    @AfterEach
+    public void cleanup() throws Exception {
+        Path camelDir = CommandLineHelper.getCamelDir();
+        if (Files.exists(camelDir)) {
+            try (Stream<Path> walk = Files.walk(camelDir)) {
+                walk.sorted(Comparator.reverseOrder()).forEach(p -> {
+                    try {
+                        Files.deleteIfExists(p);
+                    } catch (IOException e) {
+                        // best effort cleanup
+                    }
+                });
+            }
+        }
+    }
+
+    /**
+     * Writes a minimal status file so {@code findPids} can resolve the given pid by its Camel context name.
+     */
+    protected static void writeStatusFile(long pid, String contextName) throws Exception {
+        JsonObject context = new JsonObject();
+        context.put("name", contextName);
+
+        JsonObject root = new JsonObject();
+        root.put("context", context);
+        writeStatusFile(pid, root);
+    }
+
+    protected static void writeStatusFile(long pid, JsonObject root) throws Exception {
+        Path f = CommandLineHelper.getCamelDir().resolve(pid + "-status.json");
+        Files.writeString(f, root.toJson());
+    }
+
+    /**
+     * Reads back the action file an action command wrote for the given pid.
+     *
+     * @return the deserialized action JSON, or {@code null} if no action file exists
+     */
+    protected static JsonObject readActionFile(long pid) throws Exception {
+        Path f = CommandLineHelper.getCamelDir().resolve(pid + "-action.json");
+        if (!Files.exists(f)) {
+            return null;
+        }
+        return (JsonObject) Jsoner.deserialize(Files.readString(f));
+    }
+
+    /**
+     * Runs the command with ProcessHandle mocked so a single test process (TEST_PID) is discoverable and the current
+     * process is a distinct pid. Returns the exit code; any action/output files the command wrote persist on disk and
+     * can be asserted after this returns.
+     */
+    protected static int callWithSingleProcess(CamelCommand command) throws Exception {
+        try (MockedStatic<ProcessHandle> mocked = mockStatic(ProcessHandle.class)) {
+            ProcessHandle ph = mockProcessHandle(TEST_PID);
+            ProcessHandle current = mockCurrentHandle();
+            mocked.when(ProcessHandle::current).thenReturn(current);
+            mocked.when(ProcessHandle::allProcesses).thenAnswer(inv -> Stream.of(ph));
+            return command.doCall();
+        }
+    }
+
+    /**
+     * Creates a mock ProcessHandle for the test process. Info is pre-configured with empty commandLine and a fixed
+     * start instant so extractName falls through to context.name.
+     */
+    protected static ProcessHandle mockProcessHandle(long pid) {
+        ProcessHandle ph = mock(ProcessHandle.class);
+        ProcessHandle.Info info = mock(ProcessHandle.Info.class);
+        when(ph.pid()).thenReturn(pid);
+        when(ph.info()).thenReturn(info);
+        when(info.commandLine()).thenReturn(Optional.empty());
+        // lenient: only read when a status file is matched
+        lenient().when(info.startInstant()).thenReturn(Optional.of(Instant.now().minusSeconds(60)));
+        return ph;
+    }
+
+    /**
+     * Creates a mock for ProcessHandle.current(): a distinct process so the "skip current" filter in findPids does not
+     * exclude the test handle.
+     */
+    protected static ProcessHandle mockCurrentHandle() {
+        ProcessHandle current = mock(ProcessHandle.class);
+        when(current.pid()).thenReturn(CURRENT_PID);
+        return current;
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelProcessorDisableActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelProcessorDisableActionTest.java
new file mode 100644
index 000000000000..b76c4ea6bda3
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelProcessorDisableActionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelProcessorDisableActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesDisableProcessorActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelProcessorDisableAction command = new CamelProcessorDisableAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myProcessor";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("processor", action.getString("action"));
+        assertEquals("disable", action.getString("command"));
+        assertEquals("myProcessor", action.getString("id"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelProcessorDisableAction command = new CamelProcessorDisableAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelProcessorEnableActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelProcessorEnableActionTest.java
new file mode 100644
index 000000000000..baee9db35730
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelProcessorEnableActionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelProcessorEnableActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesEnableProcessorActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelProcessorEnableAction command = new CamelProcessorEnableAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myProcessor";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("processor", action.getString("action"));
+        assertEquals("enable", action.getString("command"));
+        assertEquals("myProcessor", action.getString("id"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelProcessorEnableAction command = new CamelProcessorEnableAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReloadActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReloadActionTest.java
new file mode 100644
index 000000000000..e430b21e36ae
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelReloadActionTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelReloadActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesReloadActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelReloadAction command = new CamelReloadAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("reload", action.getString("action"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelReloadAction command = new CamelReloadAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelResetStatsActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelResetStatsActionTest.java
new file mode 100644
index 000000000000..cde244e20c7e
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelResetStatsActionTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelResetStatsActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesResetStatsActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelResetStatsAction command = new CamelResetStatsAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("reset-stats", action.getString("action"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelResetStatsAction command = new CamelResetStatsAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteGroupStartActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteGroupStartActionTest.java
new file mode 100644
index 000000000000..73e64dfd11c3
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteGroupStartActionTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteGroupStartActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesStartGroupActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteGroupStartAction command = new CamelRouteGroupStartAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myGroup";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("route", action.getString("action"));
+        assertEquals("start", action.getString("command"));
+        assertEquals("myGroup", action.getString("id"));
+        // group flag distinguishes a group action from a single-route action
+        assertEquals(Boolean.TRUE, action.getBoolean("group"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteGroupStartAction command = new CamelRouteGroupStartAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteGroupStopActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteGroupStopActionTest.java
new file mode 100644
index 000000000000..74d1f5e85aed
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteGroupStopActionTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteGroupStopActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesStopGroupActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteGroupStopAction command = new CamelRouteGroupStopAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myGroup";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("route", action.getString("action"));
+        assertEquals("stop", action.getString("command"));
+        assertEquals("myGroup", action.getString("id"));
+        // group flag distinguishes a group action from a single-route action
+        assertEquals(Boolean.TRUE, action.getBoolean("group"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteGroupStopAction command = new CamelRouteGroupStopAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteResumeActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteResumeActionTest.java
new file mode 100644
index 000000000000..483d627bd979
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteResumeActionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteResumeActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesResumeActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteResumeAction command = new CamelRouteResumeAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myRoute";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("route", action.getString("action"));
+        assertEquals("resume", action.getString("command"));
+        assertEquals("myRoute", action.getString("id"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteResumeAction command = new CamelRouteResumeAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStartActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStartActionTest.java
new file mode 100644
index 000000000000..562dafd0714b
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStartActionTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteStartActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesStartActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteStartAction command = new CamelRouteStartAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myRoute";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("route", action.getString("action"));
+        assertEquals("start", action.getString("command"));
+        assertEquals("myRoute", action.getString("id"));
+    }
+
+    @Test
+    void testDefaultIdMatchesAllRoutes() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        // default id is "*" (all routes)
+        CamelRouteStartAction command = new CamelRouteStartAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action);
+        assertEquals("*", action.getString("id"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteStartAction command = new CamelRouteStartAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStopActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStopActionTest.java
new file mode 100644
index 000000000000..482f92d0191c
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStopActionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteStopActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesStopActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteStopAction command = new CamelRouteStopAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myRoute";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("route", action.getString("action"));
+        assertEquals("stop", action.getString("command"));
+        assertEquals("myRoute", action.getString("id"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteStopAction command = new CamelRouteStopAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteSuspendActionTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteSuspendActionTest.java
new file mode 100644
index 000000000000..839169b4e96b
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteSuspendActionTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.action;
+
+import org.apache.camel.dsl.jbang.core.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.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class CamelRouteSuspendActionTest extends ActionCommandTestSupport {
+
+    @Test
+    void testWritesSuspendActionForMatchingName() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteSuspendAction command = new CamelRouteSuspendAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.id = "myRoute";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        JsonObject action = readActionFile(TEST_PID);
+        assertNotNull(action, "action file should be written for the matched process");
+        assertEquals("route", action.getString("action"));
+        assertEquals("suspend", action.getString("command"));
+        assertEquals("myRoute", action.getString("id"));
+    }
+
+    @Test
+    void testNoActionWhenNameDoesNotMatch() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+
+        CamelRouteSuspendAction command = new CamelRouteSuspendAction(new CamelJBangMain().withPrinter(printer));
+        command.name = "doesNotExist";
+
+        int exit = callWithSingleProcess(command);
+
+        assertEquals(0, exit);
+        assertNull(readActionFile(TEST_PID), "no action file should be written when no process matches");
+    }
+}