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 26a31bf3939f CAMEL-23688: camel-jbang - camel cmd eval and the eval 
tools send the expression as it is (#27979)
26a31bf3939f is described below

commit 26a31bf3939f41161ef18f714f89d4c216ab94a5
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 00:28:05 2026 +0200

    CAMEL-23688: camel-jbang - camel cmd eval and the eval tools send the 
expression as it is (#27979)
    
    camel cmd eval, the eval_expression tool of camel ask and the shared
    ExpressionEvaluator (camel_eval_expression of the MCP servers) JSON-escaped
    the expression and the body before putting them in the action, which is
    JSON already. The eval action of the CLI connector does not unescape them
    (unlike the transform action, whose client escapes for it), so an
    expression with a double quote failed, such as ${body} == "hello", and a
    slash, as in every XPath, or a quote in the body became part of the text.
    
    Tests run the eval-language dev console as the connector does (the values
    of the action as they are): an expression with quotes and a slash, a JSON
    body and the eval tool of camel ask, and camel cmd eval sends both as
    given. ToolRegistryTest no longer depends on whether an integration runs
    on the machine.
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   7 +
 .../commands/action/EvalExpressionCommand.java     |   5 +-
 .../core/commands/ai/ExpressionEvaluator.java      |   5 +-
 .../dsl/jbang/core/commands/ai/ToolRegistry.java   |   3 +-
 .../commands/action/EvalExpressionResultTest.java  |  14 ++
 .../core/commands/ai/EvalInTheIntegrationTest.java | 149 +++++++++++++++++++++
 .../jbang/core/commands/ai/ToolRegistryTest.java   |  17 ++-
 7 files changed, 192 insertions(+), 8 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 9a60548ae164..cfd8900b0488 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -640,6 +640,13 @@ for an integration before Camel 4.21. `camel debug` sends 
its commands this way
   (it said the recording started), and after a recording that its results can 
be queried (it said that no samples were
   captured).
 
+=== camel-jbang - camel cmd eval with quotes and slashes
+
+`camel cmd eval`, and the tools that evaluate an expression in a running 
integration (`camel ask`, and
+`camel_eval_expression` of `camel mcp` and `camel tui --mcp` with an 
integration selected), send the expression and the
+body as they are. They escaped them, which the integration does not undo: an 
expression with a double quote failed,
+such as `+${body} == "hello"+`, and a slash (as in every XPath) or a quote in 
the body became part of the text.
+
 === camel-jpa, camel-hazelcast - an exchange marked rollback only is not 
consumed
 
 When a route marks the exchange rollback only (for example with 
`markRollbackOnly()` or `markRollbackOnlyLast()`)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
index e5bfaad8bd2f..1c4db563e78e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
@@ -163,9 +163,10 @@ public class EvalExpressionCommand extends 
ActionWatchCommand {
         root.put("action", "eval");
         root.put("language", language);
         root.put("predicate", predicate);
-        root.put("template", Jsoner.escape(template));
+        // as they are: the action file is JSON, and the integration does not 
unescape them
+        root.put("template", template);
         if (body != null) {
-            root.put("body", Jsoner.escape(body));
+            root.put("body", body);
         }
         if (headers != null) {
             JsonArray arr = new JsonArray();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
index ec4e5800740e..13b177e255db 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
@@ -91,9 +91,10 @@ public final class ExpressionEvaluator {
         String raw = ctx.executeAction("eval", root -> {
             root.put("language", lang);
             root.put("predicate", String.valueOf(predicate));
-            root.put("template", Jsoner.escape(expression));
+            // as they are: the action file is JSON, and the integration does 
not unescape them
+            root.put("template", expression);
             if (body != null) {
-                root.put("body", Jsoner.escape(body));
+                root.put("body", body);
             }
         });
         JsonObject out = null;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index 9517ddc186e0..0fd09466cf3c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -370,7 +370,8 @@ public final class ToolRegistry {
                     return ctx.executeAction("eval", root -> {
                         root.put("language", language);
                         root.put("predicate", "false");
-                        root.put("template", Jsoner.escape(expression));
+                        // as it is: the action file is JSON, and the 
integration does not unescape it
+                        root.put("template", expression);
                     });
                 }));
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionResultTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionResultTest.java
index 9ea409f748d4..2e3f3bbfd752 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionResultTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionResultTest.java
@@ -60,6 +60,20 @@ class EvalExpressionResultTest extends 
ActionCommandTestSupport {
         assertThat(variable.getString("key")).isEqualTo("total");
     }
 
+    @Test
+    void theExpressionAndBodyAreSentAsTheyAre() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+
+        // the integration does not unescape them: escaped, the quotes and 
slashes would be part of the expression
+        int exit = callWithResponse(command("orders", "--template=${body} 
contains \"/\"", "--body={\"path\":\"a/b\"}"),
+                new JsonObject(Map.of("status", "success", "result", "true")));
+
+        assertThat(exit).isEqualTo(CliExitCode.OK);
+        JsonObject action = readActionFile(TEST_PID);
+        assertThat(action.getString("template")).isEqualTo("${body} contains 
\"/\"");
+        assertThat(action.getString("body")).isEqualTo("{\"path\":\"a/b\"}");
+    }
+
     @Test
     void failedEvaluationIsAnError() throws Exception {
         writeStatusFile(TEST_PID, "orders");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/EvalInTheIntegrationTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/EvalInTheIntegrationTest.java
new file mode 100644
index 000000000000..0e35215168f6
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/EvalInTheIntegrationTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.ai;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.console.EvalLanguageDevConsole;
+import org.apache.camel.util.json.DeserializationException;
+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.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The expression tools evaluate in the selected integration: it gets the 
expression and the body as they are, as the
+ * eval action of the CLI connector reads them (it does not unescape them), 
here answered by the eval-language dev
+ * console of a Camel context of the test.
+ */
+class EvalInTheIntegrationTest {
+
+    private static final long PID = 4711L;
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+    private DefaultCamelContext context;
+    private EvalLanguageDevConsole console;
+    private final AtomicBoolean running = new AtomicBoolean(true);
+    private Thread integration;
+
+    @BeforeEach
+    void start() throws Exception {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        Files.createDirectories(CommandLineHelper.getCamelDir());
+        context = new DefaultCamelContext();
+        context.start();
+        console = new EvalLanguageDevConsole();
+        console.setCamelContext(context);
+        console.start();
+        integration = new Thread(this::answerActions, "test-integration");
+        integration.setDaemon(true);
+        integration.start();
+    }
+
+    @AfterEach
+    void stop() throws Exception {
+        running.set(false);
+        integration.join(5000);
+        context.close();
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void anExpressionWithQuotesAndSlashes() {
+        JsonObject result = ExpressionEvaluator.evaluate(ctx(), "simple", 
"${body} contains \"/\"", "orders/1");
+
+        assertThat(result.getString("evaluatedIn")).startsWith("the running 
integration");
+        assertThat(result.getString("status")).isEqualTo("ok");
+        assertThat(result.get("result")).isEqualTo("true");
+    }
+
+    @Test
+    void aBodyWithQuotes() {
+        JsonObject result = ExpressionEvaluator.evaluate(ctx(), "simple", 
"${body}", "{\"id\":1}");
+
+        assertThat(result.getString("status")).isEqualTo("ok");
+        assertThat(result.getString("result")).isEqualTo("{\"id\":1}");
+    }
+
+    @Test
+    void theEvalToolOfCamelAsk() throws Exception {
+        // an expression (not a predicate): the text with its quotes and slash
+        Object answer = ToolRegistry.execute("eval_expression", ctx(),
+                Map.of("language", "simple", "expression", "\"a/b\""));
+
+        JsonObject json = (JsonObject) Jsoner.deserialize(answer.toString());
+        assertThat(json.getString("status")).isEqualTo("success");
+        assertThat(json.getString("result")).isEqualTo("\"a/b\"");
+    }
+
+    private static ToolContext ctx() {
+        ToolContext ctx = new ToolContext();
+        ctx.selectProcess(PID);
+        return ctx;
+    }
+
+    // the eval action of the CLI connector: the values of the action as they 
are, to the eval-language console
+    private void answerActions() {
+        Path dir = CommandLineHelper.getCamelDir();
+        while (running.get()) {
+            try (Stream<Path> files = Files.list(dir)) {
+                for (Path action : files.filter(f -> 
f.getFileName().toString().startsWith(PID + "-action-")).toList()) {
+                    String text = Files.readString(action);
+                    if (text.isBlank()) {
+                        continue;
+                    }
+                    JsonObject root = (JsonObject) Jsoner.deserialize(text);
+                    Object predicate = root.get("predicate");
+                    Object result = console.call(DevConsole.MediaType.JSON, 
Map.of(
+                            "language", root.getStringOrDefault("language", 
"simple"),
+                            "predicate", predicate != null && 
Boolean.parseBoolean(predicate.toString()),
+                            "template", root.getStringOrDefault("template", 
""),
+                            "body", root.getStringOrDefault("body", ""),
+                            "headers", Map.of(), "variables", Map.of()));
+                    Path output = 
dir.resolve(action.getFileName().toString().replace("-action-", "-output-"));
+                    Path temporary = dir.resolve(PID + "-answer.tmp");
+                    Files.writeString(temporary, Jsoner.serialize(result));
+                    Files.move(temporary, output, 
StandardCopyOption.ATOMIC_MOVE);
+                    Files.deleteIfExists(action);
+                }
+                Thread.sleep(10);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return;
+            } catch (IOException | DeserializationException | RuntimeException 
e) {
+                // a half written action: the next look reads it
+            }
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
index 9416631e4236..970100c9f7db 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
@@ -16,15 +16,18 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.nio.file.Path;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -181,7 +184,7 @@ class ToolRegistryTest {
     }
 
     @Test
-    void theHttpToolsAreInternal() {
+    void theHttpToolsAreInternal(@TempDir Path home) {
         // CAMEL-25307: reached through camel_runtime_http_endpoints and 
camel_runtime_http_request
         ToolDescriptor endpoints = ToolRegistry.findTool("get_http_endpoints");
         ToolDescriptor request = ToolRegistry.findTool("http_request");
@@ -192,8 +195,16 @@ class ToolRegistryTest {
         assertFalse(request.isDestructive());
         assertFalse(ToolRegistry.authoringTools().contains(endpoints));
         assertFalse(ToolRegistry.authoringTools().contains(request));
-        assertThrows(ToolExecutionException.class,
-                () -> ToolRegistry.execute("http_request", new ToolContext(), 
Map.of("path", "/hello")));
+        // without a running integration: a home of its own, as the one of the 
machine may have one, which the tool
+        // would select
+        Path previous = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+        try {
+            assertThrows(ToolExecutionException.class,
+                    () -> ToolRegistry.execute("http_request", new 
ToolContext(), Map.of("path", "/hello")));
+        } finally {
+            CommandLineHelper.useHomeDir(previous.toString());
+        }
     }
 
     @Test

Reply via email to