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 b5e138fc0562 CAMEL-25371: camel-jbang-mcp - validating the same 
content again is a repeated call (#27422)
b5e138fc0562 is described below

commit b5e138fc0562c69797eb4f4d4581658fcc0a502a
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 12:52:23 2026 +0200

    CAMEL-25371: camel-jbang-mcp - validating the same content again is a 
repeated call (#27422)
    
    * CAMEL-25371: camel-jbang-mcp - validating the same content again is a 
repeated call, and validating the file as it is on disk says so
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25371: address review feedback
    
    - camel mcp counts camel_validate_source repeats too: the tool gets the 
connection and goes
      through callDeterministic, as the catalog tools do
    - validating content is only a repeat without a directory: with one, the 
checks read the other
      files of the project, which a fix can change
    - the content is keyed by a SHA-256 of its exact text, as whitespace can 
decide the answer (a
      newline before <?xml ...?>), and the key no longer holds the source
    - the sameAsFile note is conditional: if you meant to change the file, that 
change is not in
      this content
    - imports instead of fully qualified names
    - docs: the Repeated calls section and the camel_validate_source row
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  8 +++-
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 26 ++++++++++-
 .../jbang/core/commands/ai/RepeatedToolCalls.java  | 37 ++++++++++++---
 .../dsl/jbang/core/commands/ai/ToolDescriptor.java | 53 ++++++++++++++++++++++
 .../jbang/core/commands/ai/AuthoringToolsTest.java | 15 ++++++
 .../core/commands/ai/RepeatedToolCallsTest.java    | 48 ++++++++++++++++++++
 .../jbang/core/commands/mcp/AuthoringTools.java    |  7 ++-
 .../core/commands/mcp/AuthoringToolsTest.java      | 26 ++++++++++-
 8 files changed, 208 insertions(+), 12 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 5e23bf07a3e0..6884bc17ac33 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -324,7 +324,8 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   misspelled option such as `logLevel` instead of `loggingLevel`), endpoint 
URIs, simple expressions, and
   `camel.*` options. The routes of a Java or XML file are read into the Camel 
model without compiling them and
   get the same checks (the endpoint DSL and constants resolved, a `to` that 
needs `toD`); what the parser could
-  not read is listed under `notChecked`.
+  not read is listed under `notChecked`. When the `content` is the same as the 
file on disk, the answer has
+  `sameAsFile: true` and a `note`: an edit the agent meant to make may not be 
in what it validated.
   Takes the content, or reads the file from the project directory. With 
`camelVersion` (or the version of the
   selected integration in the TUI) the catalog and the YAML DSL schema of that 
Camel version answer, the schema
   read from its `camel-yaml-dsl` jar; without it the CLI's own, with nothing 
to download.
@@ -426,6 +427,11 @@ An agent asking one of them the same question again and 
again gets nothing new,
 stuck doing that until its tool call budget is spent. So the first two 
identical calls of a session are answered
 in full, and from the third the answer is a short note instead: that the call 
was already answered and how many
 times it was asked. The changed answer helps the agent break the loop, and the 
context is not filled with copies.
+`camel_validate_source` counts the same way when it is given `content` and no 
`directory`: the same source gets
+the same answer, and the note also says to change the line the error names. 
Validating a file on disk, or
+`content` with a `directory` (the checks then read the other files of the 
project, which a fix can change), is
+always answered in full.
+
 Calls with other arguments, and the tools that read files, logs or the running 
integration, are always answered
 in full, so re-reading a file after an edit or polling a log keeps working.
 
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 2bf98df844f5..4bce96d071f9 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
@@ -162,6 +162,11 @@ public final class AuthoringTools {
                 .param("content", "string", "The source to validate", false)
                 .param("camelVersion", "string", VERSION_DESC, false)
                 .core(true)
+                // the same content gets the same answer: a model that 
validates it again and again is stuck
+                // (CAMEL-25371). Not with a directory: the checks then read 
the other files, which a fix can change.
+                .deterministicWhen("content", "directory")
+                .repeatHint("The content is the same each time, so the answer 
is too: change the line the error names,"
+                            + " then validate the changed content, or write it 
with camel_write_file.")
                 .executor((ctx, args) -> {
                     applyVersion(ctx, args);
                     String file = required(args, "file");
@@ -176,8 +181,16 @@ public final class AuthoringTools {
                             throw new ToolExecutionException("No such file in 
the directory: " + file);
                         }
                         content = read(path);
+                        return validate(ctx, dir, file, content).toJson();
                     }
-                    return validate(ctx, dir, file, content).toJson();
+                    JsonObject result = validate(ctx, dir, file, content);
+                    if (sameAsOnDisk(ctx, directory, file, content)) {
+                        // a model that meant to change the file validates the 
old version: say so (CAMEL-25371)
+                        result.put("sameAsFile", true);
+                        result.put("note", "This content is the same as " + 
file + " on disk: if you meant to change"
+                                           + " the file, that change is not in 
this content.");
+                    }
+                    return result.toJson();
                 }));
 
         registry.accept(tool("camel_get_files",
@@ -1391,4 +1404,15 @@ public final class AuthoringTools {
         }
         return errors;
     }
+
+    /** Whether the content is exactly the file's content on disk; false when 
there is no such file. */
+    static boolean sameAsOnDisk(ToolContext ctx, String directory, String 
file, String content) {
+        try {
+            Path path = resolveFile(ctx.resolveDirectory(directory), file);
+            return Files.isRegularFile(path) && 
read(path).strip().equals(content.strip());
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
index 1ea8e2475d75..a02bd9776d4d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCalls.java
@@ -16,7 +16,11 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.HashMap;
+import java.util.HexFormat;
 import java.util.Map;
 import java.util.TreeMap;
 
@@ -51,13 +55,13 @@ public final class RepeatedToolCalls {
      * identical calls.
      */
     public synchronized JsonObject repeatOf(ToolDescriptor tool, Map<String, 
?> args) {
-        if (tool == null || !tool.isDeterministic()) {
+        if (tool == null || !tool.isDeterministic(args)) {
             return null;
         }
         if (counts.size() >= MAX_TRACKED) {
             counts.clear();
         }
-        int times = counts.merge(key(tool.name(), args), 1, Integer::sum);
+        int times = counts.merge(key(tool.name(), args, 
tool.deterministicWhen()), 1, Integer::sum);
         if (times <= FULL_ANSWERS) {
             return null;
         }
@@ -67,7 +71,8 @@ public final class RepeatedToolCalls {
         note.put("timesAsked", times);
         note.put("note", tool.name() + " was already called with these same 
arguments " + (times - 1)
                          + " times in this session and answered in full. Its 
answer does not change, so it is not sent"
-                         + " again: it is in your earlier tool result.");
+                         + " again: it is in your earlier tool result."
+                         + (tool.repeatHint() != null ? " " + 
tool.repeatHint() : ""));
         return note;
     }
 
@@ -81,16 +86,36 @@ public final class RepeatedToolCalls {
      * argument as an empty string is asking the same question as one that 
leaves it out.
      */
     static String key(String tool, Map<String, ?> args) {
+        return key(tool, args, null);
+    }
+
+    /**
+     * As {@link #key(String, Map)}, with the argument that makes the call 
deterministic (the source to validate) keyed
+     * by a digest of its exact text: whitespace can decide the answer there 
(a newline before {@code <?xml ...?>}), and
+     * the key does not hold a copy of the source.
+     */
+    static String key(String tool, Map<String, ?> args, String exact) {
         Map<String, String> sorted = new TreeMap<>();
         if (args != null) {
             for (Map.Entry<String, ?> e : args.entrySet()) {
                 Object v = e.getValue();
-                String s = v != null ? v.toString().trim() : "";
-                if (!s.isEmpty()) {
-                    sorted.put(e.getKey(), s);
+                String s = v != null ? v.toString() : "";
+                if (s.isBlank()) {
+                    continue;
                 }
+                sorted.put(e.getKey(), e.getKey().equals(exact) ? digest(s) : 
s.trim());
             }
         }
         return tool + sorted;
     }
+
+    private static String digest(String text) {
+        try {
+            byte[] hash = 
MessageDigest.getInstance("SHA-256").digest(text.getBytes(StandardCharsets.UTF_8));
+            return HexFormat.of().formatHex(hash);
+        } catch (NoSuchAlgorithmException e) {
+            // every JVM has SHA-256; the text itself is an exact key too
+            return text;
+        }
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
index a388a1180c49..08b7881473ff 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
@@ -38,6 +38,9 @@ public class ToolDescriptor {
     private boolean destructive = false;
     private boolean core = false;
     private boolean deterministic = false;
+    private String deterministicWhen;
+    private String[] deterministicUnless = new String[0];
+    private String repeatHint;
 
     public record Param(String name, String type, String description, boolean 
required) {
     }
@@ -93,6 +96,23 @@ public class ToolDescriptor {
         return this;
     }
 
+    /**
+     * Marks the tool as deterministic when the given argument is passed and 
none of the unless arguments is: validating
+     * the given content always gives the same answer, while validating the 
file on disk, or content checked against the
+     * other files of a directory, does not (CAMEL-25371).
+     */
+    public ToolDescriptor deterministicWhen(String param, String... unless) {
+        deterministicWhen = param;
+        deterministicUnless = unless != null ? unless : new String[0];
+        return this;
+    }
+
+    /** What the note of a repeated call adds for this tool: what to do 
instead of asking again. */
+    public ToolDescriptor repeatHint(String hint) {
+        repeatHint = hint;
+        return this;
+    }
+
     public ToolDescriptor executor(ToolExecutor exec) {
         this.executor = exec;
         return this;
@@ -128,6 +148,39 @@ public class ToolDescriptor {
         return deterministic;
     }
 
+    /**
+     * Whether this call gives the same answer each time: the tool is 
deterministic, or the argument that makes it so is
+     * passed without the ones that make it read other files.
+     */
+    public boolean isDeterministic(Map<String, ?> args) {
+        if (deterministic) {
+            return true;
+        }
+        if (deterministicWhen == null || !hasValue(args, deterministicWhen)) {
+            return false;
+        }
+        for (String unless : deterministicUnless) {
+            if (hasValue(args, unless)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** The argument that makes a call deterministic, or null when the tool is 
always or never deterministic. */
+    public String deterministicWhen() {
+        return deterministicWhen;
+    }
+
+    private static boolean hasValue(Map<String, ?> args, String name) {
+        Object v = args != null ? args.get(name) : null;
+        return v != null && !v.toString().isBlank();
+    }
+
+    public String repeatHint() {
+        return repeatHint;
+    }
+
     /**
      * The JSON schema of the tool's arguments, the way an MCP server lists it 
under {@code inputSchema} and an LLM
      * client sends it as the function parameters: an object with one property 
per parameter (its type and description)
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 4ab4d5def8a8..60d970a98157 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
@@ -367,4 +367,19 @@ class AuthoringToolsTest {
                 .get("errors")).get(0)).get("message")).get("body");
         assertEquals("{\"orderId\":\"ORD-1003\"}", body.getString("value"));
     }
+
+    /** CAMEL-25371: validating content that is the file on disk says the edit 
is not in it. */
+    @Test
+    void validatingTheFileAsItIsSaysSo(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("route.camel.yaml"), VALID_ROUTE);
+        JsonObject same = call("camel_validate_source", new ToolContext(),
+                Map.of("directory", dir.toString(), "file", 
"route.camel.yaml", "content", VALID_ROUTE));
+        assertTrue(Boolean.TRUE.equals(same.get("sameAsFile")), same.toJson());
+        assertTrue(same.getString("note").contains("if you meant to change the 
file, that change is not in this content"),
+                same.toJson());
+
+        JsonObject changed = call("camel_validate_source", new ToolContext(),
+                Map.of("directory", dir.toString(), "file", 
"route.camel.yaml", "content", INVALID_ROUTE));
+        assertFalse(changed.containsKey("sameAsFile"), changed.toJson());
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
index 3883b3380de2..82ea1a45aab0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/RepeatedToolCallsTest.java
@@ -87,4 +87,52 @@ class RepeatedToolCallsTest {
         calls.reset();
         assertThat(calls.repeatOf(catalogDoc, sql)).isNull();
     }
+
+    /** CAMEL-25371: validating the same content is the same question; 
validating the file on disk is not. */
+    @Test
+    void validatingTheSameContentIsARepeat() {
+        ToolDescriptor validate = 
ToolRegistry.findTool("camel_validate_source");
+        Map<String, String> args = Map.of("file", "route.camel.yaml", 
"content", "- from:\n    uri: timer:x\n");
+        assertThat(calls.repeatOf(validate, args)).isNull();
+        assertThat(calls.repeatOf(validate, args)).isNull();
+        JsonObject note = calls.repeatOf(validate, args);
+        assertThat(note).isNotNull();
+        assertThat(note.getString("note")).contains("change the line the error 
names");
+
+        Map<String, String> fromDisk = Map.of("file", "route.camel.yaml");
+        for (int i = 0; i < 4; i++) {
+            assertThat(calls.repeatOf(validate, fromDisk)).isNull();
+        }
+    }
+
+    /**
+     * CAMEL-25371: with a directory the checks read the other files of the 
project, so after a fix in another file the
+     * same content gets another answer: always answered in full.
+     */
+    @Test
+    void validatingContentAgainstADirectoryIsNotARepeat() {
+        ToolDescriptor validate = 
ToolRegistry.findTool("camel_validate_source");
+        Map<String, String> args = Map.of("directory", "/work/project", 
"file", "orders.camel.yaml",
+                "content", "- from:\n    uri: timer:x\n    steps:\n      - to: 
direct:audit\n");
+        for (int i = 0; i < 4; i++) {
+            assertThat(calls.repeatOf(validate, args)).isNull();
+        }
+    }
+
+    /**
+     * CAMEL-25371: the source is compared exactly, as whitespace can decide 
the answer (a newline before
+     * {@code <?xml}).
+     */
+    @Test
+    void contentThatOnlyDiffersInWhitespaceIsAnotherQuestion() {
+        ToolDescriptor validate = 
ToolRegistry.findTool("camel_validate_source");
+        String xml = "<?xml version=\"1.0\"?>\n<routes/>\n";
+        Map<String, String> withNewline = Map.of("file", "routes.camel.xml", 
"content", "\n" + xml);
+        assertThat(calls.repeatOf(validate, withNewline)).isNull();
+        assertThat(calls.repeatOf(validate, withNewline)).isNull();
+        // the fix (the newline removed) is validated, not answered with the 
note
+        assertThat(calls.repeatOf(validate, Map.of("file", "routes.camel.xml", 
"content", xml))).isNull();
+        // and the key does not hold the source itself
+        assertThat(RepeatedToolCalls.key("camel_validate_source", withNewline, 
"content")).doesNotContain("<routes/>");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index afd3fdb1a9d2..a4b20b6be28a 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -130,13 +130,16 @@ public class AuthoringTools {
                         + "its builder() accepts, the ways to create a class 
with no constructor). Use on content "
                         + "before writing it, or on an existing file (no 
content) to explain a reload error.")
     public JsonObject camel_validate_source(
+            McpConnection connection,
             @ToolArg(description = DIRECTORY_DESC + "; needed when no content 
is given", required = false) String directory,
             @ToolArg(description = "File name; picks the checks by extension, 
read when no content",
                      required = true) String file,
             @ToolArg(description = "The source to validate", required = false) 
String content,
             @ToolArg(description = VERSION_DESC, required = false) String 
camelVersion) {
-        return call("camel_validate_source", args("directory", directory, 
"file", file, "content", content,
-                "camelVersion", camelVersion));
+        // the same content without a directory gets the same answer: the 
third identical call gets a short note
+        // (CAMEL-25371)
+        return callDeterministic(connection, "camel_validate_source", 
args("directory", directory, "file", file,
+                "content", content, "camelVersion", camelVersion));
     }
 
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index 6ddf954e7b9c..a552525e3b22 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -103,7 +103,7 @@ class AuthoringToolsTest {
         JsonObject listed = tools.camel_get_files(dir.toString(), null);
         assertThat(listed.getInteger("totalFiles")).isEqualTo(1);
         assertThat(tools.camel_get_files(dir.toString(), 
"demo.camel.yaml").getString("content")).isEqualTo(route);
-        assertThat(tools.camel_validate_source(dir.toString(), 
"demo.camel.yaml", null, null).getBoolean("valid"))
+        assertThat(tools.camel_validate_source(null, dir.toString(), 
"demo.camel.yaml", null, null).getBoolean("valid"))
                 .isTrue();
         assertThatThrownBy(() -> tools.camel_get_files(null, null))
                 
.isInstanceOf(ToolCallException.class).hasMessageContaining("directory is 
required");
@@ -152,9 +152,10 @@ class AuthoringToolsTest {
                 boolean marked = meta != null && 
"camel.apache.org/".equals(meta.prefix())
                         && "deterministic".equals(meta.name()) && 
"true".equals(meta.value());
                 assertThat(marked).as(m.getName() + " marked 
deterministic").isEqualTo(td.isDeterministic());
+                // a tool that is deterministic for some arguments 
(camel_validate_source with content) counts too
                 boolean connected = 
Arrays.stream(m.getParameterTypes()).anyMatch(t -> t == McpConnection.class);
                 assertThat(connected).as(m.getName() + " gets the connection 
to count its repeats")
-                        .isEqualTo(td.isDeterministic());
+                        .isEqualTo(td.isDeterministic() || 
td.deterministicWhen() != null);
             }
         }
     }
@@ -183,6 +184,27 @@ class AuthoringToolsTest {
                 .get("repeated")).isNull();
     }
 
+    /** CAMEL-25371: validating the same content again is a repeat, also over 
the camel mcp server. */
+    @Test
+    void aThirdIdenticalValidationOfTheSameContentGetsAShortNote() {
+        AuthoringTools counted = new AuthoringTools();
+        counted.repeatedCalls = new RepeatedCallSessions();
+        McpConnection agent = connection("agent-1");
+        String content = "- from:\n    uri: timer:x\n    steps:\n      - to: 
log:x\n";
+
+        JsonObject first = counted.camel_validate_source(agent, null, 
"route.camel.yaml", content, null);
+        JsonObject second = counted.camel_validate_source(agent, null, 
"route.camel.yaml", content, null);
+        JsonObject third = counted.camel_validate_source(agent, null, 
"route.camel.yaml", content, null);
+
+        assertThat(first.get("repeated")).isNull();
+        assertThat(second.get("repeated")).isNull();
+        assertThat(third.getBoolean("repeated")).isTrue();
+        assertThat(third.getString("note")).contains("camel_validate_source", 
"change the line the error names");
+        // changed content is a new question
+        assertThat(counted.camel_validate_source(agent, null, 
"route.camel.yaml", content + "\n", null)
+                .get("repeated")).isNull();
+    }
+
     private static McpConnection connection(String id) {
         return (McpConnection) 
Proxy.newProxyInstance(McpConnection.class.getClassLoader(),
                 new Class<?>[] { McpConnection.class }, (proxy, method, args) 
-> "id".equals(method.getName()) ? id : null);

Reply via email to