This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-25899
in repository https://gitbox.apache.org/repos/asf/camel.git

commit b0cd6f0a36695cb228fa85efda2a643a3b842f3b
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 21 07:36:29 2026 +0200

    CAMEL-25899: camel-jbang - AI tools: the reload output of a write keeps the 
hint of a failure, and a write of a script the routes load waits for the reload
    
    - The reload outcome of a write compacts a WARN or ERROR line: the message 
ids and attempt count of the error handler,
      the exchange dumps, the packages of exception classes and long bean dumps 
go, and such a line may be 800 characters.
      The hint at the end of an exception message (such as "unmarshal it 
first") was cut off at 400 characters.
    - A write of a script or template the routes load (.groovy, .js, .py, .ftl, 
.vm, .mustache, .jslt, .jsonata) waits for
      the dev-mode reload and reports what the routes then do, as a route write 
does; it answered at once before, and the
      agent read the errors before the reload.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 19 +++++++-
 .../dsl/jbang/core/commands/ai/ReloadOutcome.java  | 55 +++++++++++++++++++++-
 .../jbang/core/commands/ai/AuthoringToolsTest.java |  9 ++++
 .../jbang/core/commands/ai/ReloadOutcomeTest.java  | 47 ++++++++++++++++++
 4 files changed, 127 insertions(+), 3 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 551e175bb5c2..7cf13591c1a8 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -1633,7 +1633,7 @@ public final class AuthoringTools {
         String processName = null;
         String sinceKey = null;
         long watchPid = -1;
-        if (SourceValidator.isValidatableFile(file)) {
+        if (SourceValidator.isValidatableFile(file) || 
reloadsWithRoutes(file)) {
             if (ctx.hasProcess()) {
                 watchPid = ctx.pid();
                 RuntimeHelper.ProcessInfo p = 
RuntimeHelper.findProcess(Long.toString(ctx.pid()));
@@ -1701,8 +1701,9 @@ public final class AuthoringTools {
                           + (output.size() > 1 ? " (and " + (output.size() - 
1) + " more in reload.output)" : "");
             }
             final String loggedAfter = logged;
+            String reloadedWhat = SourceValidator.isValidatableFile(file) ? 
"the file" : "its routes, which load the file";
             result.put("message", switch (status) {
-                case "reloaded" -> "The running integration reloaded the 
file." + loggedAfter;
+                case "reloaded" -> "The running integration reloaded " + 
reloadedWhat + "." + loggedAfter;
                 case "properties" -> "The running integration reloaded the 
properties.";
                 case "failed" -> "The running integration FAILED to reload the 
file, the route is not running; fix the"
                                  + " content and write again (see 
reload.message).";
@@ -1723,6 +1724,20 @@ public final class AuthoringTools {
         return result;
     }
 
+    /** The script and template files that routes load, such as 
resource:file:mapping.groovy. */
+    private static final List<String> ROUTE_RESOURCES
+            = List.of(".groovy", ".js", ".py", ".ftl", ".vm", ".mustache", 
".jslt", ".jsonata");
+
+    /**
+     * A file the routes load, such as the Groovy script of a setBody 
(resource:file:mapping.groovy) or a template: dev
+     * mode reloads the routes when it changes, so its write waits for that 
reload and reports what the routes then do,
+     * as the write of a route file does. Answered at once, an agent read the 
errors before the reload, and saw none.
+     */
+    static boolean reloadsWithRoutes(String fileName) {
+        String name = fileName == null ? "" : 
fileName.toLowerCase(Locale.ROOT);
+        return ROUTE_RESOURCES.stream().anyMatch(name::endsWith);
+    }
+
     /** The content of the last refused write of each file, and how many times 
in a row it was refused. */
     private record Refused(String content, int times) {
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
index cb4f1e38919e..aaae5143d500 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
@@ -21,6 +21,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.function.Supplier;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.camel.util.StopWatch;
@@ -180,6 +181,20 @@ public final class ReloadOutcome {
     private static final int OUTPUT_LINES = 5;
     private static final int OUTPUT_ERRORS = 3;
     private static final int OUTPUT_CHARS = 400;
+    /** A WARN or ERROR line may be longer: the hint of a failure comes late 
in the exception message. */
+    private static final int OUTPUT_ERROR_CHARS = 800;
+
+    // the text of the error handler around the exception, which says nothing 
the agent can act on
+    private static final Pattern FAILED_DELIVERY = Pattern.compile(
+            "^Failed delivery for \\(MessageId: \\S+ on ExchangeId: \\S+\\)(?: 
at (.+?))?\\."
+                                                                   + " 
Exhausted after delivery attempt: \\d+ caught: ");
+    private static final Pattern EXCEPTION_PACKAGE
+            = 
Pattern.compile("\\b(?:[a-z][a-z0-9_]*\\.)+([A-Z][\\w$]*(?:Exception|Error))\\b");
+    private static final Pattern ON_THE_EXCHANGE = Pattern.compile(" on the 
exchange: Exchange\\[[^\\]]*]");
+    // "Error processing exchange. Exchange[1028039098EFE4D-0000000000000009]. 
Caused by: [...]"
+    private static final Pattern EXCHANGE_ID = 
Pattern.compile("Exchange\\[[0-9A-Fa-f]+-[0-9A-Fa-f]+]\\. ");
+    private static final String ON_BEAN = " on bean: ";
+    private static final int MAX_BEAN = 80;
 
     /**
      * What the routes logged after the newest reload, waiting up to the 
timeout for the first of it: the records of the
@@ -203,6 +218,39 @@ public final class ReloadOutcome {
         return out;
     }
 
+    /**
+     * A WARN or ERROR line, as short as it can be without losing what the 
agent acts on: the message ids and the
+     * attempt count of the error handler, the dump of the exchange, the 
packages of the exception classes and the long
+     * dump of a bean go. The hint of a failure, which Camel adds at the end 
of the exception message (such as
+     * "unmarshal it first"), was cut off: the line of a failed exchange 
starts with some 250 characters of such text.
+     */
+    static String compact(String message) {
+        String m = message;
+        Matcher fd = FAILED_DELIVERY.matcher(m);
+        if (fd.find()) {
+            m = "Failed" + (fd.group(1) != null ? " at " + fd.group(1) : "") + 
": " + m.substring(fd.end());
+        }
+        m = ON_THE_EXCHANGE.matcher(m).replaceAll("");
+        m = EXCHANGE_ID.matcher(m).replaceAll("");
+        m = EXCEPTION_PACKAGE.matcher(m).replaceAll("$1");
+        // "not found on bean: [{orderId=ORD-1001, ...}, ...] of type: 
java.util.ArrayList": the start of the dump
+        StringBuilder sb = new StringBuilder();
+        int from = 0;
+        int at;
+        while ((at = m.indexOf(ON_BEAN, from)) >= 0) {
+            int start = at + ON_BEAN.length();
+            int end = m.indexOf(" of type: ", start);
+            if (end < 0) {
+                break;
+            }
+            sb.append(m, from, start);
+            sb.append(end - start > MAX_BEAN ? m.substring(start, start + 
MAX_BEAN) + "..." : m.substring(start, end));
+            from = end;
+        }
+        sb.append(m.substring(from));
+        return sb.toString();
+    }
+
     static JsonArray output(List<JsonObject> newestFirst) {
         List<String> lines = new ArrayList<>();
         for (JsonObject r : newestFirst) {
@@ -222,8 +270,13 @@ public final class ReloadOutcome {
                 // a multi-line body (an XML or JSON document) is part of what 
the route logged
                 m = m + "\n" + detail;
             }
+            int max = OUTPUT_CHARS;
+            if (!routeLog) {
+                m = compact(m);
+                max = OUTPUT_ERROR_CHARS;
+            }
             lines.add((routeLog ? "" : level.toUpperCase(Locale.ROOT) + " ")
-                      + (m.length() > OUTPUT_CHARS ? m.substring(0, 
OUTPUT_CHARS) + "..." : m));
+                      + (m.length() > max ? m.substring(0, max) + "..." : m));
         }
         // newest first in the log: the first lines the routes logged after 
the reload are at the end
         Collections.reverse(lines);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
index 2ca880ea05a0..36e5816e6f68 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
@@ -238,6 +238,15 @@ class AuthoringToolsTest {
         return (List<JsonObject>) (List<?>) errors.getCollection("errors");
     }
 
+    /** A script or template the routes load: dev mode reloads the routes when 
it changes, so its write waits. */
+    @Test
+    void aWriteOfAFileTheRoutesLoadWaitsForTheReload() {
+        
assertTrue(AuthoringTools.reloadsWithRoutes("shipment-mapping.groovy"));
+        assertTrue(AuthoringTools.reloadsWithRoutes("templates/letter.FTL"));
+        
assertFalse(AuthoringTools.reloadsWithRoutes("orders/order-1001.json"), "a data 
file is not loaded by a route");
+        assertFalse(AuthoringTools.reloadsWithRoutes("README.md"));
+    }
+
     @Test
     void writeValidatesFirstAndRefusesInvalidContent(@TempDir Path dir) throws 
IOException {
         Files.writeString(dir.resolve("demo.camel.yaml"), "- route: {}\n");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
index adb97d9222fa..befcec4ff7af 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
@@ -156,6 +156,53 @@ class ReloadOutcomeTest {
         assertEquals(7, output.size(), output.toJson());
     }
 
+    /**
+     * The line of a failed exchange starts with the message ids and the 
attempts of the error handler, and the hint of
+     * the failure comes late in the exception message: cut at 400 characters, 
the agent never saw it (enrich step 2 of
+     * the r20 runs). The line is compacted, and the hint fits.
+     */
+    @Test
+    void theHintOfAFailedExchangeFitsTheLine() {
+        List<String> failed = List.of(
+                "2026-10-21 00:30:00.000  INFO 42 --- [rReloadStrategy] 
org.apache.camel.support.RouteWatcherReloadStrategy : Routes reloaded summary 
(total:1 started:1)",
+                "2026-10-21 00:30:01.000 ERROR 42 --- [ - file://orders] 
or.errorhandler.DefaultErrorHandler : Failed delivery for"
+                                                                               
                                                                                
             + " (MessageId: 7188B2FCF80AE47-000000000000001B on ExchangeId: 
7188B2FCF80AE47-000000000000001B) at"
+                                                                               
                                                                                
             + " incoming-orders[log14] enrich-and-dedupe.camel.yaml:42. 
Exhausted after delivery attempt: 1 caught:"
+                                                                               
                                                                                
             + " org.apache.camel.language.bean.RuntimeBeanExpressionException: 
Failed to invoke method: [name] on"
+                                                                               
                                                                                
             + " org.apache.camel.component.file.GenericFile due to: 
java.lang.IndexOutOfBoundsException: Key: name"
+                                                                               
                                                                                
             + " not found in bean: GenericFile[C-482.json] of type: 
org.apache.camel.component.file.GenericFile"
+                                                                               
                                                                                
             + " using OGNL path [[name]] (the value is the file as read (a 
GenericFile), not its data: unmarshal it"
+                                                                               
                                                                                
             + " first, for example with json, then read the key; a variable is 
unmarshalled in place with"
+                                                                               
                                                                                
             + " variableSend and variableReceive set to its name, such as 
unmarshal: {json: {}, variableSend:"
+                                                                               
                                                                                
             + " customer, variableReceive: customer})");
+
+        JsonArray output = ReloadOutcome.output(records(BEFORE, failed));
+
+        assertEquals(1, output.size(), output.toJson());
+        String line = output.get(0).toString();
+        assertTrue(line.startsWith("ERROR Failed at incoming-orders[log14] 
enrich-and-dedupe.camel.yaml:42:"
+                                   + " RuntimeBeanExpressionException: Failed 
to invoke method: [name]"),
+                line);
+        // the whole hint, not cut
+        assertTrue(line.endsWith("variableSend: customer, variableReceive: 
customer})"), line);
+        assertTrue(!line.contains("MessageId") && !line.contains("Exhausted 
after"), line);
+    }
+
+    @Test
+    void theDumpOfAnExchangeAndOfALongBeanGo() {
+        String m = ReloadOutcome.compact("Method with name: orderId not found 
on bean: ["
+                                         + "{orderId=ORD-1001, 
customer=C-482}, ".repeat(5)
+                                         + "] of type: java.util.ArrayList on 
the exchange: Exchange[ABC-1]");
+        assertTrue(m.startsWith("Method with name: orderId not found on bean: 
[{orderId=ORD-1001, customer=C-482}"), m);
+        assertTrue(m.endsWith("... of type: java.util.ArrayList"), m);
+        assertEquals("Error processing exchange. Caused by: 
[NullPointerException - Cannot get property 'id' on null object]",
+                ReloadOutcome.compact("Error processing exchange. 
Exchange[1028039098EFE4D-0000000000000009]. Caused by:"
+                                      + " [java.lang.NullPointerException - 
Cannot get property 'id' on null object]"));
+        // a short dump stays
+        assertEquals("Key: name not found in bean: GenericFile[C-482.json] of 
type: x",
+                ReloadOutcome.compact("Key: name not found in bean: 
GenericFile[C-482.json] of type: x"));
+    }
+
     @Test
     void whatWasLoggedBeforeTheNewestReloadIsNotOutput() {
         assertEquals(List.of("Hello Camel"), 
List.copyOf(ReloadOutcome.output(records(BEFORE))));

Reply via email to