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 6a8963e32a06 CAMEL-26876: camel-jbang - AI tools: a Citrus test that 
verifies a log line with regex escapes is refused with the line to write 
(#29698)
6a8963e32a06 is described below

commit 6a8963e32a06b0baa7c5125d885d23f5101a55c6
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 13:09:21 2026 +0100

    CAMEL-26876: camel-jbang - AI tools: a Citrus test that verifies a log line 
with regex escapes is refused with the line to write (#29698)
    
    The logMessage of a camel cli (or jbang) verify action is plain text:
    Citrus checks that the integration logged a line that contains it as
    written. In r32 of the local-model benchmark a model wrote it as a regular
    expression (line\(s\)), and camel test run waited 60 attempts for a line
    with the backslashes in it. A backslash before ( ) [ ] { } . * + ? ^ $ |
    now refuses the write, and the message ends with the line to write without
    those backslashes. A backslash before another character is left alone.
    The answer to a log or assert-log action, which points to the verify
    action, now says that it matches the text as written (plain text, not a
    pattern).
    
    
    Claude-Session: https://claude.ai/code/session_016Y4dDi5iQNgtHrDdz2NpwF
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../jbang/core/commands/ai/CitrusTestChecks.java   | 52 ++++++++++++-
 .../core/commands/ai/CitrusTestChecksTest.java     | 85 +++++++++++++++++++++-
 2 files changed, 134 insertions(+), 3 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecks.java
index 0c435d48fb54..49583572526c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecks.java
@@ -45,6 +45,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.MissingNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.networknt.schema.Error;
 import com.networknt.schema.Schema;
@@ -105,6 +106,11 @@ public final class CitrusTestChecks {
             "matches", "match", "regex", "pattern", "like", "startsWith", 
"endsWith");
     /** A URL: the client part (scheme, host and port), and the path. */
     private static final Pattern URL = 
Pattern.compile("^([A-Za-z][\\w+.-]*://[^/?#]+)(.*)$");
+    /**
+     * A backslash before a character of a regular expression, such as \( in 
line\(s\), or before a second backslash:
+     * the two backslashes are taken as a pair, so the character after them is 
not escaped.
+     */
+    private static final Pattern REGEX_ESCAPE = 
Pattern.compile("\\\\([\\\\()\\[\\]{}.*+?^$|])");
 
     private static volatile TestSchema schema;
 
@@ -526,6 +532,9 @@ public final class CitrusTestChecks {
             String name = scalar(keys.get(0).getKeyNode());
             items.put(at, name);
             itemNodes.put(at, item);
+            if ("camel".equals(name)) {
+                checkLogMessage(at);
+            }
             JsonNode action = tree.at(at);
             if (nested && !action.isMissingNode()) {
                 describe(schema.action().validate(action), at);
@@ -548,16 +557,55 @@ public final class CitrusTestChecks {
             }
             if (camelCaseWords(name).contains("log") && 
schema.actions().contains("echo")
                     && schema.actions().contains("camel")) {
-                // log:, assert-log: or assertLog: in 8 of the 11 tests a 
local model wrote (test-citrus-orders)
+                // log:, assert-log: or assertLog: in 8 of the 11 tests a 
local model wrote (test-citrus-orders); a
+                // model that wrote assert-log: {pattern: ...} kept the 
escapes of the pattern in logMessage (r32)
                 String cli = 
schema.model().at("/definitions/TestActions/properties/camel/properties/cli").isMissingNode()
                         ? "jbang" : "cli";
                 return answer + ": echo: {message: \"...\"} writes a message 
to the test log, and camel: {" + cli
                        + ": {verify: {integration: \"<name>\", logMessage: 
\"...\"}}} checks that the integration"
-                       + " logged a line";
+                       + " logged a line that contains that text as written 
(plain text, not a pattern)";
             }
             return answer + "; the actions are " + String.join(", ", 
schema.actions());
         }
 
+        /**
+         * The logMessage of a camel cli (or jbang) verify action is plain 
text: Citrus checks that the integration
+         * logged a line that contains it as written. In r32 of the 
local-model benchmark, three of seven runs of
+         * test-citrus-orders step 1 wrote it as a regular expression, 
line\(s\) for line(s), and camel test run waited
+         * 60 attempts for a line with the backslashes in it (the model had 
first written assert-log: {pattern: ...},
+         * which is not a Citrus action, and kept the escapes). The line to 
write has the text without the backslash of
+         * each escape. A backslash before another character, as in a Windows 
path or an escaped quote, is left alone.
+         */
+        private void checkLogMessage(String at) {
+            JsonNode camel = 
schema.model().at("/definitions/TestActions/properties/camel");
+            for (String cli : List.of("cli", "jbang")) {
+                String pointer = at + "/camel/" + cli + "/verify/logMessage";
+                JsonNode value = tree.at(pointer);
+                if (!value.isTextual() || !hasPath(camel, cli, "verify", 
"logMessage")) {
+                    continue;
+                }
+                List<String> escapes = new ArrayList<>();
+                String text = 
REGEX_ESCAPE.matcher(value.asText()).replaceAll(m -> {
+                    boolean pair = "\\".equals(m.group(1));
+                    if (!pair) {
+                        escapes.add(m.group());
+                    }
+                    return Matcher.quoteReplacement(pair ? m.group() : 
m.group(1));
+                });
+                if (escapes.isEmpty()) {
+                    continue;
+                }
+                String answer = where(pointer) + " is plain text, not a 
regular expression: Citrus checks that the"
+                                + " integration logged a line that contains it 
as written, so the backslash"
+                                + (escapes.size() == 1 ? "" : "es") + " in " + 
and(escapes.stream().distinct().toList())
+                                + " would have to be in the log too.";
+                String written = keyLines(pointer, TextNode.valueOf(text));
+                Node key = keyNode(pointer);
+                add(key != null ? line(key) : YamlPointerLines.line(root, 
pointer, null),
+                        answer + (written != null ? written : " Write 
logMessage: " + quoted(text)));
+            }
+        }
+
         /**
          * The schema errors in words: what the noise of the branches of a 
oneOf leaves, without the type errors of the
          * values Citrus converts, and the keys an object does not take in one 
message, with the keys it takes.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecksTest.java
index f33c3257660e..3ce668b62abe 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CitrusTestChecksTest.java
@@ -25,6 +25,8 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Stream;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.junit.jupiter.api.Named;
 import org.junit.jupiter.api.Test;
@@ -44,11 +46,13 @@ import static org.assertj.core.api.Assertions.assertThat;
 class CitrusTestChecksTest {
 
     private static final Path ORDERS = 
Path.of("src/test/resources/citrus-orders");
+    private static final ObjectMapper YAML = new ObjectMapper(new 
YAMLFactory());
 
     /** What is said of an action named log: what to write to log a message, 
and to check the log of the integration. */
     private static final String LOG_HINT = "echo: {message: \"...\"} writes a 
message to the test log, and camel: {cli:"
                                            + " {verify: {integration: 
\"<name>\", logMessage: \"...\"}}} checks that the"
-                                           + " integration logged a line";
+                                           + " integration logged a line that 
contains that text as written (plain"
+                                           + " text, not a pattern)";
 
     /** What is said of the request and its response written as one http 
action, before the lines to write. */
     private static final String TWO_ACTIONS = ". An http action of a client 
either sends the request (sendRequest) or"
@@ -485,6 +489,85 @@ class CitrusTestChecksTest {
         return String.join("\n", lines);
     }
 
+    /** A test that checks the log of the integration: the key of the camel 
action, and the logMessage as written. */
+    private static final String VERIFY_LOG = """
+            name: orders.citrus.it
+            actions:
+              - camel:
+                  %s:
+                    verify:
+                      integration: "orders"
+                      logMessage: %s
+            """;
+
+    static Stream<Arguments> logMessagesWithRegexEscapes() {
+        return Stream.of(
+                // as the model wrote it in r32
+                Arguments.of("cli", "\"Order ORD-1001 accepted for C-482 with 
2 line\\\\(s\\\\)\""),
+                // in single quotes and without quotes, the backslash is 
written once
+                Arguments.of("cli", "'Order ORD-1001 accepted for C-482 with 2 
line\\(s\\)'"),
+                Arguments.of("cli", "Order ORD-1001 accepted for C-482 with 2 
line\\(s\\)"),
+                // jbang is the key Citrus had before cli
+                Arguments.of("jbang", "'Order ORD-1001 accepted for C-482 with 
2 line\\(s\\)'"));
+    }
+
+    /**
+     * r32 of the local-model benchmark, test-citrus-orders step 1: in three 
of seven runs the model checked the log
+     * line with a regular expression, logMessage: "... line\\(s\\)" (it had 
first written assert-log: {pattern: ...}),
+     * and camel test run waited 60 attempts for a line with the backslashes 
in it. logMessage is plain text: the line
+     * to write has the text without them, and written, the test is valid.
+     */
+    @ParameterizedTest(name = "{0}: {1}")
+    @MethodSource("logMessagesWithRegexEscapes")
+    void aLogMessageWithRegexEscapesIsPlainText(String cli, String value) 
throws Exception {
+        String test = VERIFY_LOG.formatted(cli, value);
+        List<String> msgs = CitrusTestChecks.validate(test);
+
+        assertThat(msgs).containsExactly("Line 7: camel." + cli + 
".verify.logMessage is plain text, not a regular"
+                                         + " expression: Citrus checks that 
the integration logged a line that contains"
+                                         + " it as written, so the backslashes 
in \\( and \\) would have to be in the"
+                                         + " log too. Write line 7 as:\n"
+                                         + "          logMessage: \"Order 
ORD-1001 accepted for C-482 with 2 line(s)\"");
+        String fixed = written(test, msgs.get(0));
+        assertThat(CitrusTestChecks.validate(fixed)).isEmpty();
+        assertThat(YAML.readTree(fixed).at("/actions/0/camel/" + cli + 
"/verify/logMessage").asText())
+                .isEqualTo("Order ORD-1001 accepted for C-482 with 2 line(s)");
+    }
+
+    /**
+     * The line to write keeps a backslash before another character, as in a 
Windows path; the text is then in single
+     * quotes, where YAML takes a backslash as written.
+     */
+    @Test
+    void theLineToWriteKeepsTheOtherBackslashes() throws Exception {
+        String test = VERIFY_LOG.formatted("cli", "'Wrote ORD-1001\\.json to 
C:\\orders\\out'");
+        List<String> msgs = CitrusTestChecks.validate(test);
+
+        assertThat(msgs).containsExactly("Line 7: camel.cli.verify.logMessage 
is plain text, not a regular expression:"
+                                         + " Citrus checks that the 
integration logged a line that contains it as"
+                                         + " written, so the backslash in \\. 
would have to be in the log too. Write"
+                                         + " line 7 as:\n"
+                                         + "          logMessage: 'Wrote 
ORD-1001.json to C:\\orders\\out'");
+        String fixed = written(test, msgs.get(0));
+        assertThat(CitrusTestChecks.validate(fixed)).isEmpty();
+        
assertThat(YAML.readTree(fixed).at("/actions/0/camel/cli/verify/logMessage").asText())
+                .isEqualTo("Wrote ORD-1001.json to C:\\orders\\out");
+    }
+
+    /**
+     * Text without a regular expression escape is checked as written: a 
backslash before another character (a Windows
+     * path, an escaped quote), and two backslashes before a (, as JSON text 
in the log has them.
+     */
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "\"Order ORD-1001 accepted for C-482 with 2 line(s)\"",
+            "'Wrote ORD-1001.json to C:\\orders\\out'",
+            "'Order \\\"ORD-1001\\\" accepted'",
+            "'{\"file\": \"C:\\\\orders\\\\(old)\"}'" })
+    void aLogMessageWithoutRegexEscapesIsValid(String value) {
+        assertThat(CitrusTestChecks.validate(VERIFY_LOG.formatted("cli", 
value))).isEmpty();
+    }
+
     @Test
     void aTestCutOffInTheMiddleOfAnAction() throws Exception {
         List<String> msgs = validate(ORDERS.resolve("cut-off.citrus.it.yaml"));

Reply via email to