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 a38641fd302a CAMEL-23688: camel-jbang - Fix infra ps/get for 
hyphenated service names and add unit tests
a38641fd302a is described below

commit a38641fd302a8c5721bff03e464c07c584bce82c
Author: Adriano Machado <[email protected]>
AuthorDate: Fri Jun 26 01:45:15 2026 -0400

    CAMEL-23688: camel-jbang - Fix infra ps/get for hyphenated service names 
and add unit tests
    
    Fix infra ps and infra get commands dropping hyphenated service names
    (e.g. hive-mq) by consolidating pid-file parsing into shared
    serviceNameFromPidFile/pidFromPidFile helpers that use indexOf/lastIndexOf
    instead of split("-"). Add unit tests for DataWeave lexer/parser,
    CatalogKamelet sorting, VersionSet config, and InfraGet/InfraPs.
    
    Closes #24257
    
    Co-Authored-By: Claude <[email protected]>
---
 .../core/commands/infra/InfraBaseCommand.java      |  30 ++-
 .../dsl/jbang/core/commands/infra/InfraGet.java    |   3 +-
 .../dsl/jbang/core/commands/infra/InfraPs.java     |   2 +-
 .../core/commands/catalog/CatalogKameletTest.java  | 140 ++++++++++
 .../commands/config/BaseConfigTestSupport.java     |   5 +-
 .../commands/infra/InfraCommandTestSupport.java    |  68 +++++
 .../jbang/core/commands/infra/InfraGetTest.java    |  92 +++++++
 .../dsl/jbang/core/commands/infra/InfraPsTest.java |  77 ++++++
 .../commands/transform/DataWeaveLexerTest.java     | 153 +++++++++++
 .../commands/transform/DataWeaveParserTest.java    | 286 +++++++++++++++++++++
 .../core/commands/version/VersionSetTest.java      | 124 +++++++++
 11 files changed, 972 insertions(+), 8 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
index ec6e143772de..474cdb266f6a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraBaseCommand.java
@@ -52,7 +52,6 @@ import 
org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
 import org.apache.camel.dsl.jbang.core.model.InfraBaseDTO;
 import org.apache.camel.support.PatternHelper;
-import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.json.DeserializationException;
 import org.apache.camel.util.json.Jsoner;
 import picocli.CommandLine;
@@ -98,8 +97,8 @@ public abstract class InfraBaseCommand extends CamelCommand {
                     .toList();
             for (Path pidFile : pidFiles) {
                 String fn = pidFile.getFileName().toString();
-                String sn = fn.substring(fn.indexOf("-") + 1, 
fn.lastIndexOf('-'));
-                String pid = fn.substring(fn.lastIndexOf("-") + 1, 
fn.lastIndexOf('.'));
+                String sn = serviceNameFromPidFile(fn);
+                String pid = pidFromPidFile(fn);
                 if (pid.equals(pattern) || PatternHelper.matchPattern(sn, 
pattern)) {
                     pids.put(Long.valueOf(pid), pidFile);
                 }
@@ -111,6 +110,29 @@ public abstract class InfraBaseCommand extends 
CamelCommand {
         return pids;
     }
 
+    /**
+     * Extracts the service name from an {@code infra-<service>-<pid>.json} 
pid file name. The service name may itself
+     * contain hyphens (for example {@code hive-mq}), so it spans everything 
between the first and the last hyphen
+     * rather than just the second hyphen-delimited segment.
+     *
+     * @param  pidFileName the pid file name, such as {@code 
infra-hive-mq-1234.json}.
+     * @return             the service name, such as {@code hive-mq}.
+     */
+    protected static String serviceNameFromPidFile(String pidFileName) {
+        return pidFileName.substring(pidFileName.indexOf('-') + 1, 
pidFileName.lastIndexOf('-'));
+    }
+
+    /**
+     * Extracts the pid from an {@code infra-<service>-<pid>.json} pid file 
name. The pid is the segment between the
+     * last hyphen and the file extension, which is robust to service names 
that themselves contain hyphens.
+     *
+     * @param  pidFileName the pid file name, such as {@code 
infra-hive-mq-1234.json}.
+     * @return             the pid, such as {@code 1234}.
+     */
+    protected static String pidFromPidFile(String pidFileName) {
+        return pidFileName.substring(pidFileName.lastIndexOf('-') + 1, 
pidFileName.lastIndexOf('.'));
+    }
+
     protected boolean showPidColumn() {
         return false;
     }
@@ -223,7 +245,7 @@ public abstract class InfraBaseCommand extends CamelCommand 
{
             Files.createDirectories(p);
             for (String s : Objects.requireNonNull(p.toFile().list())) {
                 if (s.startsWith("infra-" + key + "-") && s.endsWith(".json")) 
{
-                    return FileUtil.stripExt(s.split("-")[2]);
+                    return pidFromPidFile(s);
                 }
             }
         } catch (Exception e) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
index 9e4af0a1ba2a..705dc3734410 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGet.java
@@ -44,8 +44,7 @@ public class InfraGet extends InfraBaseCommand {
         int i = 0;
         for (var e : pids.entrySet()) {
             Path pidFile = e.getValue();
-            String fn = pidFile.getFileName().toString();
-            String sn = fn.substring(fn.indexOf("-") + 1, fn.lastIndexOf('-'));
+            String sn = 
serviceNameFromPidFile(pidFile.getFileName().toString());
             if (i > 0) {
                 printer().println("");
             }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
index d1808324e456..f761be085a8b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPs.java
@@ -62,7 +62,7 @@ public class InfraPs extends InfraBaseCommand {
                     .toList();
 
             for (Path pidFile : pidFiles) {
-                String runningServiceName = 
pidFile.getFileName().toString().split("-")[1];
+                String runningServiceName = 
serviceNameFromPidFile(pidFile.getFileName().toString());
                 runningAliases.add(runningServiceName);
             }
         } catch (IOException e) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
new file mode 100644
index 000000000000..9fc4d5dda497
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.catalog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for the row sorting logic of {@link CatalogKamelet}.
+ *
+ * The {@code doCall()} method downloads the Kamelet catalog from Maven and is 
therefore not unit tested here; the
+ * deterministic, user-visible ordering produced by {@link 
CatalogKamelet#sortRow} is what these tests pin down.
+ */
+class CatalogKameletTest extends CamelCommandBaseTestSupport {
+
+    private static KameletModel model(String name, String type, String 
supportLevel, String description) {
+        KameletModel m = new KameletModel();
+        m.name = name;
+        m.type = type;
+        m.supportLevel = supportLevel;
+        m.description = description;
+        return m;
+    }
+
+    private List<KameletModel> sampleRows() {
+        List<KameletModel> rows = new ArrayList<>();
+        // intentionally unordered, mixed case, so a no-op comparator would be 
detectable
+        rows.add(model("kafka-source", "source", "Stable", "Reads from 
Kafka"));
+        rows.add(model("Aws-S3-sink", "sink", "Preview", "Writes to S3"));
+        rows.add(model("timer-source", "action", "Stable", "A timer based 
source"));
+        return rows;
+    }
+
+    private CatalogKamelet command() {
+        return new CatalogKamelet(new CamelJBangMain().withPrinter(printer));
+    }
+
+    private static List<String> names(List<KameletModel> rows) {
+        return rows.stream().map(r -> r.name).toList();
+    }
+
+    @Test
+    void shouldSortByNameCaseInsensitiveByDefault() {
+        CatalogKamelet cmd = command();
+        cmd.sort = "name";
+
+        List<KameletModel> rows = sampleRows();
+        rows.sort(cmd::sortRow);
+
+        // case-insensitive: "Aws-S3-sink" sorts before "kafka-source"
+        assertEquals(List.of("Aws-S3-sink", "kafka-source", "timer-source"), 
names(rows));
+    }
+
+    @Test
+    void shouldReverseOrderWhenSortKeyHasLeadingMinus() {
+        CatalogKamelet cmd = command();
+        cmd.sort = "-name";
+
+        List<KameletModel> rows = sampleRows();
+        rows.sort(cmd::sortRow);
+
+        assertEquals(List.of("timer-source", "kafka-source", "Aws-S3-sink"), 
names(rows));
+    }
+
+    @Test
+    void shouldSortByType() {
+        CatalogKamelet cmd = command();
+        cmd.sort = "type";
+
+        List<KameletModel> rows = sampleRows();
+        rows.sort(cmd::sortRow);
+
+        // ordered by type: action, sink, source
+        assertEquals(List.of("timer-source", "Aws-S3-sink", "kafka-source"), 
names(rows));
+    }
+
+    @Test
+    void shouldSortBySupportLevelViaBothAliases() {
+        List<KameletModel> rowsLevel = sampleRows();
+        CatalogKamelet level = command();
+        level.sort = "level";
+        rowsLevel.sort(level::sortRow);
+
+        List<KameletModel> rowsSupportLevel = sampleRows();
+        CatalogKamelet supportLevel = command();
+        supportLevel.sort = "support-level";
+        rowsSupportLevel.sort(supportLevel::sortRow);
+
+        // "level" and "support-level" are documented aliases and must produce 
identical ordering
+        assertEquals(names(rowsLevel), names(rowsSupportLevel));
+        // "Preview" sorts before "Stable"; the two Stable rows keep stable 
relative order
+        assertEquals(List.of("Aws-S3-sink", "kafka-source", "timer-source"), 
names(rowsLevel));
+    }
+
+    @Test
+    void shouldSortByDescription() {
+        CatalogKamelet cmd = command();
+        cmd.sort = "description";
+
+        List<KameletModel> rows = sampleRows();
+        rows.sort(cmd::sortRow);
+
+        assertEquals(List.of("timer-source", "kafka-source", "Aws-S3-sink"), 
names(rows));
+    }
+
+    @Test
+    void shouldLeaveOrderUnchangedForUnknownSortKey() {
+        CatalogKamelet cmd = command();
+        cmd.sort = "bogus";
+
+        List<KameletModel> original = sampleRows();
+        List<KameletModel> rows = sampleRows();
+        rows.sort(cmd::sortRow);
+
+        // an unrecognized key yields a comparator that returns 0, so the 
input order is preserved
+        assertEquals(names(original), names(rows));
+        assertEquals(0, cmd.sortRow(original.get(0), original.get(1)),
+                "unknown sort key must produce a no-op comparator");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
index 909ced04975b..384aa8e2211e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/config/BaseConfigTestSupport.java
@@ -27,7 +27,10 @@ import org.junit.jupiter.api.AfterEach;
 public class BaseConfigTestSupport extends CamelCommandBaseTestSupport {
 
     @AfterEach
-    void removeLocalConfigFile() throws IOException {
+    void removeConfigFiles() throws IOException {
         Files.deleteIfExists(Paths.get(CommandLineHelper.LOCAL_USER_CONFIG));
+        // remove the global config file written under target by 
UserConfigHelper.createUserConfig, so it does not
+        // leak into later tests in the same JVM. Target the build directory 
explicitly to never touch the real home.
+        Files.deleteIfExists(Paths.get("target", 
CommandLineHelper.USER_CONFIG));
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraCommandTestSupport.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraCommandTestSupport.java
new file mode 100644
index 000000000000..8e587c3c4bd2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraCommandTestSupport.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.infra;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Base class for infra command tests that read the {@code 
infra-<service>-<pid>.json} pid files from the Camel
+ * directory. It redirects the home directory to an isolated folder under 
{@code target} and clears it between tests,
+ * following the same convention as {@code ProcessCommandTestSupport}.
+ */
+abstract class InfraCommandTestSupport extends CamelCommandBaseTestSupport {
+
+    @BeforeEach
+    @Override
+    public void setup() throws Exception {
+        super.setup();
+        CommandLineHelper.useHomeDir("target/test-infra-cmd");
+        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 pid file named {@code infra-<service>-<pid>.json} into the 
Camel directory with the given JSON content.
+     */
+    protected static void writePidFile(String service, long pid, String 
content) throws IOException {
+        Path pidFile = CommandLineHelper.getCamelDir().resolve("infra-" + 
service + "-" + pid + ".json");
+        Files.writeString(pidFile, content);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGetTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGetTest.java
new file mode 100644
index 000000000000..a0ce688bd631
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraGetTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.infra;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link InfraGet}.
+ *
+ * The command resolves running infra services from the pid files ({@code 
infra-<service>-<pid>.json}) stored in the
+ * Camel directory, then prints a header plus the file contents for each 
match. The home directory is redirected to an
+ * isolated folder by {@link InfraCommandTestSupport} so the tests are 
independent of any real local services.
+ */
+class InfraGetTest extends InfraCommandTestSupport {
+
+    @Test
+    void shouldPrintServiceHeaderAndContentByName() throws Exception {
+        writePidFile("kafka", 1234, "{\"port\":9092}");
+
+        InfraGet command = new InfraGet(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "kafka";
+        int exit = command.doCall();
+
+        assertEquals(0, exit);
+        String out = printer.getOutput();
+        // the service name and pid are parsed back out of the file name
+        assertTrue(out.contains("Service kafka (PID: 1234)"), "should print 
the service header, was: " + out);
+        assertTrue(out.contains("{\"port\":9092}"), "should print the pid file 
contents, was: " + out);
+    }
+
+    @Test
+    void shouldMatchByExactPid() throws Exception {
+        writePidFile("kafka", 1234, "{\"port\":9092}");
+        writePidFile("ftp", 5678, "{\"port\":21}");
+
+        InfraGet command = new InfraGet(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "5678";
+        int exit = command.doCall();
+
+        assertEquals(0, exit);
+        String out = printer.getOutput();
+        // a numeric argument matches the pid, not the service name
+        assertTrue(out.contains("Service ftp (PID: 5678)"), "should match the 
ftp service by pid, was: " + out);
+        assertFalse(out.contains("kafka"), "should not print the unmatched 
kafka service, was: " + out);
+    }
+
+    @Test
+    void shouldParseHyphenatedServiceName() throws Exception {
+        // service names may contain hyphens; the whole name between first and 
last hyphen must be recovered
+        writePidFile("hive-mq", 1234, "{\"port\":1883}");
+
+        InfraGet command = new InfraGet(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "hive-mq";
+        int exit = command.doCall();
+
+        assertEquals(0, exit);
+        String out = printer.getOutput();
+        assertTrue(out.contains("Service hive-mq (PID: 1234)"),
+                "should recover the full hyphenated service name, was: " + 
out);
+    }
+
+    @Test
+    void shouldPrintNothingWhenNoServiceMatches() throws Exception {
+        writePidFile("kafka", 1234, "{\"port\":9092}");
+
+        InfraGet command = new InfraGet(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "redis";
+        int exit = command.doCall();
+
+        assertEquals(0, exit);
+        assertEquals("", printer.getOutput(), "no matching service must 
produce no output");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
new file mode 100644
index 000000000000..3ae0a669890c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/infra/InfraPsTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.infra;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link InfraPs}.
+ *
+ * Unlike {@code infra list}, which prints every known service, {@code infra 
ps} only shows the services that have a
+ * running pid file ({@code infra-<service>-<pid>.json}) in the Camel 
directory, and includes the PID column. The home
+ * directory is redirected to an isolated folder by {@link 
InfraCommandTestSupport} so the tests do not depend on
+ * locally running services.
+ */
+class InfraPsTest extends InfraCommandTestSupport {
+
+    @Test
+    void shouldOnlyListRunningServicesWithPid() throws Exception {
+        // kafka is running, minio is not
+        writePidFile("kafka", 1234, "{}");
+
+        InfraPs command = new InfraPs(new 
CamelJBangMain().withPrinter(printer));
+        int exit = command.doCall();
+
+        assertEquals(0, exit);
+        String out = printer.getOutput();
+        assertTrue(out.contains("kafka"), "running kafka service should be 
listed, was: " + out);
+        assertTrue(out.contains("1234"), "running kafka service should show 
its pid, was: " + out);
+        assertFalse(out.contains("minio"), "non-running services must be 
filtered out, was: " + out);
+    }
+
+    @Test
+    void shouldListRunningHyphenatedService() throws Exception {
+        // regression: the running alias is parsed with the full hyphenated 
name (e.g. hive-mq), not just the
+        // second hyphen-delimited segment ("hive"), otherwise the catalog row 
would be filtered out
+        writePidFile("hive-mq", 1234, "{}");
+
+        InfraPs command = new InfraPs(new 
CamelJBangMain().withPrinter(printer));
+        int exit = command.doCall();
+
+        assertEquals(0, exit);
+        String out = printer.getOutput();
+        assertTrue(out.contains("hive-mq"), "running hyphenated service should 
be listed, was: " + out);
+        assertTrue(out.contains("1234"), "running hyphenated service should 
show its pid, was: " + out);
+    }
+
+    @Test
+    void shouldListNoServicesWhenNoneRunning() throws Exception {
+        // no pid files written: the service table must be cleared
+        InfraPs command = new InfraPs(new 
CamelJBangMain().withPrinter(printer));
+        int exit = command.doCall();
+
+        assertEquals(0, exit);
+        String out = printer.getOutput();
+        assertFalse(out.contains("kafka"), "no service rows should be printed 
when nothing is running, was: " + out);
+        assertFalse(out.contains("minio"), "no service rows should be printed 
when nothing is running, was: " + out);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexerTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexerTest.java
new file mode 100644
index 000000000000..9395ac07424c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexerTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.transform;
+
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.Token;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.TokenType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for {@link DataWeaveLexer}.
+ *
+ * These assert the concrete token stream (types, values and positions) for 
representative inputs, focusing on the
+ * tricky, ambiguous cases where a tokenizer typically goes wrong: 
multi-character operators, negative numbers vs the
+ * minus operator, the {@code ---} header separator, and comment skipping.
+ */
+class DataWeaveLexerTest {
+
+    private static List<Token> tokenize(String input) {
+        return new DataWeaveLexer(input).tokenize();
+    }
+
+    private static List<TokenType> types(String input) {
+        return tokenize(input).stream().map(Token::type).toList();
+    }
+
+    @Test
+    void shouldAlwaysAppendEofToken() {
+        List<Token> tokens = tokenize("");
+        assertEquals(1, tokens.size());
+        assertEquals(TokenType.EOF, tokens.get(0).type());
+    }
+
+    @Test
+    void shouldClassifyKeywordsAndIdentifiers() {
+        // true/false -> BOOLEAN, null -> NULL_LIT, and/or/not -> logical ops, 
everything else -> IDENTIFIER
+        assertEquals(
+                List.of(TokenType.BOOLEAN, TokenType.BOOLEAN, 
TokenType.NULL_LIT,
+                        TokenType.AND, TokenType.OR, TokenType.NOT,
+                        TokenType.IDENTIFIER, TokenType.EOF),
+                types("true false null and or not payload"));
+    }
+
+    @Test
+    void shouldReadStringWithBothQuoteStylesAndKeepEscapes() {
+        List<Token> tokens = tokenize("\"hello\" 'world' \"a\\\"b\"");
+        assertEquals(TokenType.STRING, tokens.get(0).type());
+        assertEquals("hello", tokens.get(0).value());
+        assertEquals(TokenType.STRING, tokens.get(1).type());
+        assertEquals("world", tokens.get(1).value());
+        // the escaped quote is preserved verbatim inside the string value
+        assertEquals("a\\\"b", tokens.get(2).value());
+    }
+
+    @Test
+    void shouldReadDecimalNumber() {
+        List<Token> tokens = tokenize("3.14");
+        assertEquals(TokenType.NUMBER, tokens.get(0).type());
+        assertEquals("3.14", tokens.get(0).value());
+    }
+
+    @Test
+    void shouldTreatLeadingMinusAsNegativeNumberWhenNotAfterValue() {
+        // at the start of input the minus binds to the number
+        List<Token> tokens = tokenize("-5");
+        assertEquals(TokenType.NUMBER, tokens.get(0).type());
+        assertEquals("-5", tokens.get(0).value());
+    }
+
+    @Test
+    void shouldTreatMinusBetweenValuesAsOperator() {
+        // "5 - 3": the minus follows a value, so it is the subtraction 
operator, not a sign
+        assertEquals(
+                List.of(TokenType.NUMBER, TokenType.MINUS, TokenType.NUMBER, 
TokenType.EOF),
+                types("5 - 3"));
+    }
+
+    @Test
+    void shouldTreatMinusAfterOpeningParenAsNegativeNumber() {
+        // "(-3)": previous char is '(', not value-like, so the minus binds to 
the number
+        assertEquals(
+                List.of(TokenType.LPAREN, TokenType.NUMBER, TokenType.RPAREN, 
TokenType.EOF),
+                types("(-3)"));
+        assertEquals("-3", tokenize("(-3)").get(1).value());
+    }
+
+    @Test
+    void shouldDistinguishSingleAndDoubleCharacterOperators() {
+        assertEquals(
+                List.of(TokenType.PLUSPLUS, TokenType.PLUS, TokenType.ARROW, 
TokenType.MINUS,
+                        TokenType.EQ, TokenType.ASSIGN, TokenType.NEQ, 
TokenType.GE, TokenType.GT,
+                        TokenType.LE, TokenType.LT, TokenType.EOF),
+                types("++ + -> - == = != >= > <= <"));
+    }
+
+    @Test
+    void shouldRecognizeHeaderSeparatorAtLineStart() {
+        // the --- on its own line is a header separator, while the negative 
number afterwards stays a number
+        List<TokenType> ts = types("%dw 2.0\n---\n-5");
+        assertEquals(
+                List.of(TokenType.PERCENT, TokenType.IDENTIFIER, 
TokenType.NUMBER,
+                        TokenType.HEADER_SEPARATOR, TokenType.NUMBER, 
TokenType.EOF),
+                ts);
+    }
+
+    @Test
+    void shouldSkipLineAndBlockComments() {
+        String input = """
+                // a line comment
+                payload /* inline block */ + 1
+                """;
+        assertEquals(
+                List.of(TokenType.IDENTIFIER, TokenType.PLUS, 
TokenType.NUMBER, TokenType.EOF),
+                types(input));
+    }
+
+    @Test
+    void shouldTrackLineAndColumnPositions() {
+        // "ab\n  cd": first token at 1:1, second at line 2 after two spaces 
-> 2:3
+        List<Token> tokens = tokenize("ab\n  cd");
+        assertEquals(1, tokens.get(0).line());
+        assertEquals(1, tokens.get(0).col());
+        assertEquals("ab", tokens.get(0).value());
+        assertEquals(2, tokens.get(1).line());
+        assertEquals(3, tokens.get(1).col());
+        assertEquals("cd", tokens.get(1).value());
+    }
+
+    @Test
+    void shouldSkipUnknownCharactersInsteadOfFailing() {
+        // '@' is not a recognized token; the lexer drops it and keeps going
+        assertEquals(
+                List.of(TokenType.IDENTIFIER, TokenType.IDENTIFIER, 
TokenType.EOF),
+                types("a @ b"));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParserTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParserTest.java
new file mode 100644
index 000000000000..8802531c2932
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParserTest.java
@@ -0,0 +1,286 @@
+/*
+ * 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.transform;
+
+import java.util.List;
+
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.ArrayLit;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.BinaryOp;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.DefaultExpr;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.FieldAccess;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.FunctionCall;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Identifier;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.IfElse;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.IndexAccess;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Lambda;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.LambdaShorthand;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.MapExpr;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.NumberLit;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.ObjectLit;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Parens;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Script;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.StringLit;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.TypeCoercion;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.UnaryOp;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.Unsupported;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.VarDecl;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.Token;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link DataWeaveParser}.
+ *
+ * The parser is recursive descent and best-effort (it never throws on 
malformed input). These tests assert the shape of
+ * the produced {@link DataWeaveAst} for representative scripts, with emphasis 
on operator precedence, postfix
+ * collection operations, header parsing, and graceful handling of unsupported 
constructs. The AST records are exercised
+ * transitively here rather than in a separate test, since they carry no logic 
of their own.
+ */
+class DataWeaveParserTest {
+
+    private static DataWeaveAst parseExpr(String src) {
+        var tokens = new DataWeaveLexer(src).tokenize();
+        return new DataWeaveParser(tokens).parseExpressionOnly();
+    }
+
+    private static DataWeaveAst parseScript(String src) {
+        var tokens = new DataWeaveLexer(src).tokenize();
+        return new DataWeaveParser(tokens).parse();
+    }
+
+    // ── Header ──
+
+    @Test
+    void shouldParseHeaderVersionAndOutputType() {
+        String dw = """
+                %dw 2.0
+                output application/json
+                ---
+                { greeting: "hello" }
+                """;
+        Script script = assertInstanceOf(Script.class, parseScript(dw));
+        assertEquals("2.0", script.header().version());
+        assertEquals("application/json", script.header().outputType());
+        assertTrue(script.header().inputs().isEmpty(), "no input declarations 
expected");
+        ObjectLit body = assertInstanceOf(ObjectLit.class, script.body());
+        assertEquals(1, body.entries().size());
+    }
+
+    @Test
+    void shouldParseInputDeclarations() {
+        String dw = """
+                %dw 2.0
+                input payload application/xml
+                output application/json
+                ---
+                payload
+                """;
+        Script script = assertInstanceOf(Script.class, parseScript(dw));
+        assertEquals(1, script.header().inputs().size());
+        assertEquals("payload", script.header().inputs().get(0).name());
+        assertEquals("application/xml", 
script.header().inputs().get(0).mediaType());
+    }
+
+    @Test
+    void shouldDefaultVersionWhenNoHeaderPresent() {
+        // an expression with no %dw header still yields a Script with the 
default 2.0 version
+        Script script = assertInstanceOf(Script.class, parseScript("payload"));
+        assertEquals("2.0", script.header().version());
+        assertNull(script.header().outputType());
+        assertInstanceOf(Identifier.class, script.body());
+    }
+
+    // ── Literals and field access ──
+
+    @Test
+    void shouldParseLiterals() {
+        assertEquals("42", assertInstanceOf(NumberLit.class, 
parseExpr("42")).value());
+        assertEquals("hi", assertInstanceOf(StringLit.class, 
parseExpr("\"hi\"")).value());
+        assertTrue(assertInstanceOf(DataWeaveAst.BooleanLit.class, 
parseExpr("true")).value());
+        assertInstanceOf(DataWeaveAst.NullLit.class, parseExpr("null"));
+    }
+
+    @Test
+    void shouldParseFieldAccessAndIndexAccessChain() {
+        // payload.items[0] -> IndexAccess(FieldAccess(Identifier(payload), 
items), NumberLit(0))
+        IndexAccess idx = assertInstanceOf(IndexAccess.class, 
parseExpr("payload.items[0]"));
+        assertEquals("0", assertInstanceOf(NumberLit.class, 
idx.index()).value());
+        FieldAccess fa = assertInstanceOf(FieldAccess.class, idx.object());
+        assertEquals("items", fa.field());
+        assertEquals("payload", assertInstanceOf(Identifier.class, 
fa.object()).name());
+    }
+
+    // ── Precedence ──
+
+    @Test
+    void shouldGiveMultiplicationHigherPrecedenceThanAddition() {
+        // 1 + 2 * 3 -> BinaryOp(+, 1, BinaryOp(*, 2, 3))
+        BinaryOp add = assertInstanceOf(BinaryOp.class, parseExpr("1 + 2 * 
3"));
+        assertEquals("+", add.op());
+        assertEquals("1", assertInstanceOf(NumberLit.class, 
add.left()).value());
+        BinaryOp mul = assertInstanceOf(BinaryOp.class, add.right());
+        assertEquals("*", mul.op());
+        assertEquals("2", assertInstanceOf(NumberLit.class, 
mul.left()).value());
+        assertEquals("3", assertInstanceOf(NumberLit.class, 
mul.right()).value());
+    }
+
+    @Test
+    void shouldGiveAndHigherPrecedenceThanOr() {
+        // a and b or c -> BinaryOp(or, BinaryOp(and, a, b), c)
+        BinaryOp or = assertInstanceOf(BinaryOp.class, parseExpr("a and b or 
c"));
+        assertEquals("or", or.op());
+        assertEquals("c", assertInstanceOf(Identifier.class, 
or.right()).name());
+        BinaryOp and = assertInstanceOf(BinaryOp.class, or.left());
+        assertEquals("and", and.op());
+    }
+
+    @Test
+    void shouldParseConcatOperator() {
+        BinaryOp op = assertInstanceOf(BinaryOp.class, parseExpr("\"a\" ++ 
\"b\""));
+        assertEquals("++", op.op());
+    }
+
+    @Test
+    void shouldParseParenthesizedGrouping() {
+        Parens parens = assertInstanceOf(Parens.class, parseExpr("(a + b)"));
+        assertEquals("+", assertInstanceOf(BinaryOp.class, 
parens.expr()).op());
+    }
+
+    // ── Object / array literals ──
+
+    @Test
+    void shouldParseObjectLiteralWithStaticAndDynamicKeys() {
+        ObjectLit obj = assertInstanceOf(ObjectLit.class, parseExpr("{ name: 
payload.x, (k): v }"));
+        assertEquals(2, obj.entries().size());
+        // static key
+        assertEquals("name", assertInstanceOf(Identifier.class, 
obj.entries().get(0).key()).name());
+        assertFalse(obj.entries().get(0).dynamic());
+        // dynamic key parsed from a parenthesized expression
+        assertTrue(obj.entries().get(1).dynamic());
+        assertEquals("k", assertInstanceOf(Identifier.class, 
obj.entries().get(1).key()).name());
+    }
+
+    @Test
+    void shouldParseArrayLiteral() {
+        ArrayLit arr = assertInstanceOf(ArrayLit.class, parseExpr("[1, 2, 
3]"));
+        assertEquals(3, arr.elements().size());
+        assertEquals("2", assertInstanceOf(NumberLit.class, 
arr.elements().get(1)).value());
+    }
+
+    // ── Collection operations / lambdas ──
+
+    @Test
+    void shouldParseMapWithExplicitLambda() {
+        MapExpr map = assertInstanceOf(MapExpr.class, parseExpr("payload map 
((item) -> item.name)"));
+        assertEquals("payload", assertInstanceOf(Identifier.class, 
map.collection()).name());
+        Lambda lambda = assertInstanceOf(Lambda.class, map.lambda());
+        assertEquals(1, lambda.params().size());
+        assertEquals("item", lambda.params().get(0).name());
+        assertInstanceOf(FieldAccess.class, lambda.body());
+    }
+
+    @Test
+    void shouldParseMapWithDollarShorthand() {
+        MapExpr map = assertInstanceOf(MapExpr.class, parseExpr("payload map 
$.name"));
+        LambdaShorthand sh = assertInstanceOf(LambdaShorthand.class, 
map.lambda());
+        assertEquals(List.of("name"), sh.fields());
+    }
+
+    // ── Function calls ──
+
+    @Test
+    void shouldParseBuiltinFunctionCall() {
+        FunctionCall call = assertInstanceOf(FunctionCall.class, 
parseExpr("sizeOf(payload)"));
+        assertEquals("sizeOf", call.name());
+        assertEquals(1, call.args().size());
+        assertEquals("payload", assertInstanceOf(Identifier.class, 
call.args().get(0)).name());
+    }
+
+    // ── Type coercion ──
+
+    @Test
+    void shouldParseTypeCoercionWithoutFormat() {
+        TypeCoercion tc = assertInstanceOf(TypeCoercion.class, parseExpr("x as 
String"));
+        assertEquals("String", tc.type());
+        assertNull(tc.format());
+    }
+
+    @Test
+    void shouldParseTypeCoercionWithFormat() {
+        TypeCoercion tc = assertInstanceOf(TypeCoercion.class, parseExpr("x as 
Date {format: \"yyyy-MM-dd\"}"));
+        assertEquals("Date", tc.type());
+        assertEquals("yyyy-MM-dd", tc.format());
+    }
+
+    // ── Control flow ──
+
+    @Test
+    void shouldParseIfElse() {
+        IfElse ie = assertInstanceOf(IfElse.class, parseExpr("if (a) b else 
c"));
+        assertEquals("a", assertInstanceOf(Identifier.class, 
ie.condition()).name());
+        assertEquals("b", assertInstanceOf(Identifier.class, 
ie.thenExpr()).name());
+        assertEquals("c", assertInstanceOf(Identifier.class, 
ie.elseExpr()).name());
+    }
+
+    @Test
+    void shouldParseDefaultExpression() {
+        DefaultExpr def = assertInstanceOf(DefaultExpr.class, parseExpr("a 
default b"));
+        assertEquals("a", assertInstanceOf(Identifier.class, 
def.expr()).name());
+        assertEquals("b", assertInstanceOf(Identifier.class, 
def.fallback()).name());
+    }
+
+    @Test
+    void shouldParseVarDeclarationWithTrailingBody() {
+        // "var x = 1 x" -> VarDecl(x, NumberLit(1), body=Identifier(x))
+        VarDecl var = assertInstanceOf(VarDecl.class, parseExpr("var x = 1 
x"));
+        assertEquals("x", var.name());
+        assertEquals("1", assertInstanceOf(NumberLit.class, 
var.value()).value());
+        assertEquals("x", assertInstanceOf(Identifier.class, 
var.body()).name());
+    }
+
+    // ── Unary ──
+
+    @Test
+    void shouldParseLogicalNot() {
+        UnaryOp not = assertInstanceOf(UnaryOp.class, parseExpr("not a"));
+        assertEquals("not", not.op());
+        assertEquals("a", assertInstanceOf(Identifier.class, 
not.operand()).name());
+    }
+
+    // ── Graceful degradation ──
+
+    @Test
+    void shouldRepresentMatchExpressionAsUnsupported() {
+        Unsupported node = assertInstanceOf(Unsupported.class, 
parseExpr("payload match { 1 }"));
+        assertEquals("match expression", node.reason());
+        assertTrue(node.originalText().startsWith("match"),
+                "original text should be retained, was: " + 
node.originalText());
+    }
+
+    @Test
+    void shouldNotThrowOnUnbalancedInput() {
+        // expect() silently skips a missing RPAREN, so a best-effort AST is 
still produced
+        List<Token> tokens = new DataWeaveLexer("(a + b").tokenize();
+        assertEquals(DataWeaveLexer.TokenType.EOF, tokens.get(tokens.size() - 
1).type());
+        assertInstanceOf(Parens.class, parseExpr("(a + b"));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
new file mode 100644
index 000000000000..23fc0f5a171f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/version/VersionSetTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.version;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.commands.UserConfigHelper;
+import org.apache.camel.dsl.jbang.core.commands.config.BaseConfigTestSupport;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.RuntimeType;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link VersionSet}.
+ *
+ * The command persists the selected Camel version, repositories and runtime 
into the user configuration property file.
+ * These tests pin down which keys are written (only the non-null ones), the 
{@code --reset} behavior, and the choice
+ * between the global and local configuration file. Config file cleanup is 
inherited from {@link BaseConfigTestSupport}.
+ */
+class VersionSetTest extends BaseConfigTestSupport {
+
+    @Test
+    void shouldStoreVersionInGlobalConfig() throws Exception {
+        UserConfigHelper.createUserConfig("");
+
+        VersionSet command = new VersionSet(new 
CamelJBangMain().withPrinter(printer));
+        command.version = "4.10.0";
+        command.doCall();
+
+        CommandLineHelper.loadProperties(properties -> {
+            assertEquals("4.10.0", properties.get("camel-version"));
+            // repos and runtime were not requested, so they must not be 
written
+            assertFalse(properties.containsKey("repos"), "repos must not be 
set when no --repo is given");
+            assertFalse(properties.containsKey("runtime"), "runtime must not 
be set when no --runtime is given");
+        });
+    }
+
+    @Test
+    void shouldStoreVersionRepoAndRuntimeTogether() throws Exception {
+        UserConfigHelper.createUserConfig("");
+
+        VersionSet command = new VersionSet(new 
CamelJBangMain().withPrinter(printer));
+        command.version = "4.10.0";
+        command.repo = "https://repo.example.org/maven";;
+        command.runtime = RuntimeType.springBoot;
+        command.doCall();
+
+        CommandLineHelper.loadProperties(properties -> {
+            assertEquals("4.10.0", properties.get("camel-version"));
+            assertEquals("https://repo.example.org/maven";, 
properties.get("repos"));
+            // runtime is stored using its canonical runtime() name, not the 
enum constant
+            assertEquals(RuntimeType.springBoot.runtime(), 
properties.get("runtime"));
+        });
+    }
+
+    @Test
+    void shouldLeaveExistingConfigUntouchedWhenNothingProvided() throws 
Exception {
+        UserConfigHelper.createUserConfig("""
+                camel-version=4.9.0
+                """);
+
+        VersionSet command = new VersionSet(new 
CamelJBangMain().withPrinter(printer));
+        // no version, repo, runtime or reset: doCall must not change the 
stored value
+        command.doCall();
+
+        CommandLineHelper.loadProperties(properties -> assertEquals("4.9.0", 
properties.get("camel-version")));
+    }
+
+    @Test
+    void shouldRemoveAllSettingsOnReset() throws Exception {
+        UserConfigHelper.createUserConfig("""
+                camel-version=4.9.0
+                repos=https://repo.example.org/maven
+                runtime=spring-boot
+                other=keep-me
+                """);
+
+        VersionSet command = new VersionSet(new 
CamelJBangMain().withPrinter(printer));
+        command.reset = true;
+        command.doCall();
+
+        CommandLineHelper.loadProperties(properties -> {
+            assertFalse(properties.containsKey("camel-version"), 
"camel-version must be removed on reset");
+            assertFalse(properties.containsKey("repos"), "repos must be 
removed on reset");
+            assertFalse(properties.containsKey("runtime"), "runtime must be 
removed on reset");
+            // unrelated settings are preserved: reset only clears the three 
version keys
+            assertEquals("keep-me", properties.get("other"));
+        });
+    }
+
+    @Test
+    void shouldStoreVersionInLocalConfigWhenNotGlobal() throws Exception {
+        VersionSet command = new VersionSet(new 
CamelJBangMain().withPrinter(printer));
+        command.version = "4.10.0";
+        command.global = false;
+        command.doCall();
+
+        // the value lands in the local config file, not the global one
+        
assertTrue(Files.exists(Paths.get(CommandLineHelper.LOCAL_USER_CONFIG)),
+                "local config file should have been created");
+        CommandLineHelper.loadProperties(properties -> assertEquals("4.10.0", 
properties.get("camel-version")), true);
+    }
+
+}

Reply via email to