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 be0857aab86c chore: camel-jbang - the AI tools see the beans in
src/main/resources, catch a route id another file has, and say why a start
failed (#28001)
be0857aab86c is described below
commit be0857aab86c93fe0a03e86a43072350cc407c4c
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 17 18:16:32 2026 +0200
chore: camel-jbang - the AI tools see the beans in src/main/resources,
catch a route id another file has, and say why a start failed (#28001)
Two fixes for the AI tools, found with the local-model benchmark, where the
step that adds a Java class and declares it as a bean failed in 8 of 10
runs.
- Bean declarations also come from the YAML in src/main/resources, which
camel
run --source-dir loads, so a reference to a bean declared there is no
longer
refused, and a file with only beans (or another top-level entry of the
YAML
DSL) is listed with the route files.
- The validator refuses a route id that a route in another YAML or XML file
of
the project has, and names that file. A failed camel_run or restart says
its
cause first: the error camel run prints as it exits, else the first
exception
logged at ERROR (the cause of the camel_run answer).
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/ai/AuthoringTools.java | 8 ++-
.../core/commands/ai/IntegrationLauncher.java | 66 ++++++++++++++++++-
.../jbang/core/commands/ai/SourceValidator.java | 75 +++++++++++++++++++---
.../jbang/core/commands/ai/AuthoringToolsTest.java | 15 +++++
.../core/commands/ai/IntegrationLauncherTest.java | 50 +++++++++++++++
.../commands/ai/SourceValidatorBeanRefsTest.java | 40 ++++++++++++
.../commands/ai/SourceValidatorRoutesTest.java | 33 ++++++++++
7 files changed, 275 insertions(+), 12 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 048a1faa09de..20e0f79b2692 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
@@ -81,9 +81,15 @@ public final class AuthoringTools {
static final Set<String> SKIPPED_DIRS = Set.of(
"target", "build", "out", "node_modules", ".git", ".mvn", ".idea",
".vscode", ".gradle", ".settings",
".camel-jbang");
+ // the top-level entries of the YAML DSL: a file with only beans
(beans.yaml) is loaded with the routes too, and a
+ // model that did not see it among the route files took the beans it
declares for missing
private static final Pattern YAML_ROUTE = Pattern.compile(
"(?m)^\\s*-\\s*(route|from|rest|routeTemplate|route-template|templatedRoute|templated-route"
- +
"|routeConfiguration|route-configuration|kamelet)\\s*:");
+ +
"|routeConfiguration|route-configuration|kamelet|beans"
+ +
"|errorHandler|error-handler|onException|on-exception"
+ +
"|intercept|interceptFrom|intercept-from"
+ +
"|interceptSendToEndpoint|intercept-send-to-endpoint"
+ +
"|onCompletion|on-completion)\\s*:");
private AuthoringTools() {
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index 551d2637c772..031b5a1fc738 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
@@ -61,6 +62,13 @@ public final class IntegrationLauncher {
/** A stack frame, or the count of the frames a cause shares with the
exception it caused. */
private static final Pattern FRAME = Pattern.compile("^\\s*(at
\\S.*|\\.\\.\\. \\d+ (more|common frames omitted))$");
+ /** The line camel run prints when it exits with an error. */
+ private static final Pattern CLI_ERROR = Pattern.compile("^ERROR: (.+)$");
+ /** An exception of the console output with its message. */
+ private static final Pattern EXCEPTION_LINE
+ = Pattern.compile("^(?:Caused by: )?[\\w.$]+(?:Exception|Error):
(.+)$");
+ /** The longest cause put first in an answer; the output that follows has
all of it. */
+ private static final int MAX_CAUSE = 500;
/**
* The processes this JVM started from a directory with --source-dir, so a
copy that has not written its status file
@@ -142,10 +150,16 @@ public final class IntegrationLauncher {
long deadline = System.currentTimeMillis() + STARTUP_TIMEOUT_MS;
while (System.currentTimeMillis() < deadline) {
if (!process.isAlive()) {
+ String cause = failureCause(output);
result.put("status", "failed");
result.put("exitCode", process.exitValue());
+ if (cause != null) {
+ result.put("cause", cause);
+ }
result.put("output", failureOutput(output));
- result.put("message", "camel run exited before the integration
started; the output has the reason");
+ result.put("message", cause != null
+ ? "camel run exited before the integration started: "
+ cause
+ : "camel run exited before the integration started;
the output has the reason");
return result;
}
RuntimeHelper.ProcessInfo info = findByPid(pid);
@@ -272,6 +286,49 @@ public final class IntegrationLauncher {
return null;
}
+ /**
+ * What made a run fail to start, in one line: the error camel run prints
as it exits ({@code ERROR: ...}), else the
+ * message of the first exception after a record logged at ERROR. The
output shows it among the startup records,
+ * under the banner; the answer puts it first. A model retried camel_run
three times with "Duplicate route ids
+ * detected" in the output, and never changed the file that held the copy
of the route. Null when the output has
+ * neither.
+ */
+ static String failureCause(Path output) {
+ List<String> lines;
+ try {
+ lines = readText(output).lines().map(String::strip).toList();
+ } catch (IOException e) {
+ return null;
+ }
+ String cause = null;
+ // camel run prints its error last
+ for (int i = lines.size() - 1; i >= 0 && cause == null; i--) {
+ Matcher m = CLI_ERROR.matcher(lines.get(i));
+ if (m.matches()) {
+ cause = m.group(1);
+ }
+ }
+ for (int i = 0; i < lines.size() && cause == null; i++) {
+ if (lines.get(i).contains(" ERROR ")) {
+ for (int j = i + 1; j < Math.min(lines.size(), i + 5) && cause
== null; j++) {
+ Matcher m = EXCEPTION_LINE.matcher(lines.get(j));
+ if (m.matches()) {
+ cause = m.group(1);
+ }
+ }
+ if (cause == null) {
+ int at = lines.get(i).indexOf(" : ");
+ cause = at > 0 ? lines.get(i).substring(at + 3) :
lines.get(i);
+ }
+ }
+ }
+ if (cause == null || cause.isBlank()) {
+ return null;
+ }
+ cause = cause.strip();
+ return cause.length() > MAX_CAUSE ? cause.substring(0, MAX_CAUSE) +
"..." : cause;
+ }
+
/**
* The console output of a run that failed to start, as camel_get_log
shows a log (CAMEL-25364): each record keeps
* its message, the lines of its exceptions (the exception, its causes,
and a message over several lines such as the
@@ -565,10 +622,13 @@ public final class IntegrationLauncher {
}
if (!p.isAlive()) {
boolean kept = stoppedByRestart(directory, name);
+ String cause = failureCause(output);
throw new ToolExecutionException(
"Restart of " + (name != null ? name : "pid " + pid) +
" failed with exit code "
- + p.exitValue()
- + ", so it is stopped now:
fix the cause below, then camel_run starts it again"
+ + p.exitValue() + ", so it is
stopped now"
+ + (cause != null
+ ? ". The cause: " +
cause + "\nFix it" : ": fix the cause below")
+ + ", then camel_run starts it
again"
+ (kept ? " (directory " +
directory + ") under the same name, " + name : "")
+ ".\n" +
failureOutput(output));
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 7982bbfe663a..19001ee8989f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -143,6 +143,8 @@ public final class SourceValidator {
msgs.addAll(OpenApiPathParams.validate(content, directory));
// an insert-or-update in the dialect of another database than
the datasource (CAMEL-25557)
msgs.addAll(SqlDialectChecks.validate(content, directory));
+ // a route id that a route in another file of the project has:
Camel does not start
+ msgs.addAll(duplicateRouteIds(fileName, content, catalog,
directory));
if (checkConsumers) {
// a direct: or seda: endpoint no route of the application
consumes (CAMEL-24955)
msgs.addAll(EndpointConsumerChecks.validateYamlConsumers(content, directory,
fileName, catalog));
@@ -379,20 +381,20 @@ public final class SourceValidator {
if (directory == null || !Files.isDirectory(directory)) {
return NONE;
}
+ Path excluded = excludeFile != null ?
directory.resolve(excludeFile).normalize() : null;
try (var stream = Files.list(directory)) {
List<Path> files = new
ArrayList<>(stream.filter(Files::isRegularFile).toList());
// a class of the project may also sit in the Maven layout,
src/main/java/<package>/<Class>.java, which
// camel run --source-dir compiles too: without it a bean of
that class was refused as not found
- Path mavenSources = directory.resolve("src/main/java");
- if (Files.isDirectory(mavenSources)) {
- try (var walk = Files.walk(mavenSources)) {
- walk.filter(Files::isRegularFile).filter(q ->
q.getFileName().toString().endsWith(".java"))
- .forEach(files::add);
- }
- }
+ addMavenFiles(directory.resolve("src/main/java"), ".java",
files);
+ // and the YAML in src/main/resources, which camel run
--source-dir loads as well (it loads the whole
+ // directory): a model that put the class in src/main/java
wrote beans.yaml there, 6 times out of 8 in
+ // a benchmark night, and each reference to the bean was then
refused as not declared
+ addMavenFiles(directory.resolve("src/main/resources"),
".yaml", files);
+ addMavenFiles(directory.resolve("src/main/resources"), ".yml",
files);
for (Path p : files) {
String fn = p.getFileName().toString();
- if (fn.equals(excludeFile)) {
+ if (p.normalize().equals(excluded)) {
continue;
}
String lower = fn.toLowerCase(Locale.ROOT);
@@ -430,6 +432,63 @@ public final class SourceValidator {
}
return new BeanDeclarations(names, classes, signatures,
publicMethods, publicMethodNamesByClass);
}
+
+ /** Adds the files with the suffix in a directory of the Maven layout,
subdirectories included, if it exists. */
+ private static void addMavenFiles(Path dir, String suffix, List<Path>
files) throws IOException {
+ if (Files.isDirectory(dir)) {
+ try (var walk = Files.walk(dir)) {
+ walk.filter(Files::isRegularFile).filter(q ->
q.getFileName().toString().endsWith(suffix))
+ .forEach(files::add);
+ }
+ }
+ }
+ }
+
+ /**
+ * The routes of the content whose id a route in another YAML or XML file
of the project has. camel run --source-dir
+ * loads every file of the directory, and Camel does not start with two
routes of one id ("Duplicate route ids
+ * detected"). A model asked to declare a bean in beans.yaml copied the
route there too: the write went ahead, the
+ * restart failed, and the model ran camel_run again until its calls ran
out.
+ */
+ static List<String> duplicateRouteIds(String fileName, String content,
CamelCatalog catalog, Path directory) {
+ // the id of each route of the content, with its line
+ Map<String, Integer> ids = new LinkedHashMap<>();
+ try {
+ for (ProjectRoutes.Route r : ProjectRoutes.parse(fileName,
content, catalog)) {
+ if ("route".equals(r.kind()) && r.id() != null &&
!r.id().isBlank()) {
+ ids.putIfAbsent(r.id(), r.line());
+ }
+ }
+ } catch (RuntimeException e) {
+ return List.of();
+ }
+ if (ids.isEmpty()) {
+ return List.of();
+ }
+ Path self = directory.resolve(fileName).normalize();
+ List<String> msgs = new ArrayList<>();
+ for (Path p : AuthoringTools.projectFiles(directory)) {
+ String rel = AuthoringTools.relativePath(directory, p);
+ String type = AuthoringTools.fileType(rel);
+ if (p.normalize().equals(self) || !("yaml".equals(type) ||
"xml".equals(type))
+ || !"route".equals(AuthoringTools.fileKind(p, rel, type)))
{
+ continue;
+ }
+ try {
+ for (ProjectRoutes.Route r : ProjectRoutes.parse(rel,
Files.readString(p), catalog)) {
+ Integer line = "route".equals(r.kind()) && r.id() != null
? ids.remove(r.id()) : null;
+ if (line != null) {
+ msgs.add("Line " + line + ": route id '" + r.id() + "'
is already the id of a route in " + rel
+ + ", and Camel does not start with two routes
of one id (camel run loads every file of"
+ + " the directory): change the route in " +
rel + " rather than adding a copy here, or"
+ + " give this route another id");
+ }
+ }
+ } catch (IOException | RuntimeException e) {
+ // an unreadable or unparsable file has no route to compare
+ }
+ }
+ return msgs;
}
/**
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 60d970a98157..86a0883b39d3 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
@@ -211,6 +211,21 @@ class AuthoringToolsTest {
assertEquals(VALID_ROUTE,
Files.readString(dir.resolve("src/main/resources/camel/x.camel.yaml")));
}
+ @Test
+ void aBeansFileIsListedWithTheRouteFiles(@TempDir Path dir) throws
IOException {
+ // beans.yaml has no route, yet camel run loads it with the routes: a
model that did not see it among the route
+ // files took the beans it declares for missing
+ Files.writeString(dir.resolve("demo.camel.yaml"), VALID_ROUTE);
+ Files.createDirectories(dir.resolve("src/main/resources"));
+ Files.writeString(dir.resolve("src/main/resources/beans.yaml"), """
+ - beans:
+ - name: orderNumber
+ type: camel.example.OrderNumber
+ """);
+ JsonObject list = call("camel_get_files", new ToolContext(),
Map.of("directory", dir.toString()));
+ assertEquals(List.of("demo.camel.yaml",
"src/main/resources/beans.yaml"), list.getCollection("routeFiles"));
+ }
+
@Test
void aMavenProjectListsItsRouteFilesFirstAndReadsByRelativePath(@TempDir
Path dir) throws IOException {
Files.writeString(dir.resolve("pom.xml"), "<project/>");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
index 0d7994b61f72..fcfbd42a33ac 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
@@ -101,6 +101,56 @@ class IntegrationLauncherTest {
assertThat(text.indexOf("is
starting")).isLessThan(text.indexOf("Failed to create route"));
}
+ @Test
+ void failureCauseIsTheErrorCamelRunPrintsAsItExits(@TempDir Path dir)
throws Exception {
+ // the console of camel run with a copy of the route in beans.yaml,
next to the route file
+ String duplicate = "Failed to create route because: Duplicate route
ids detected: order-generator. Please correct"
+ + " ids to be unique among all your routes.";
+ StringBuilder out = new StringBuilder();
+ out.append("Running integration with the following configuration:\n
--kamelets-version=4.22.1\n");
+ out.append("2026-10-17 11:03:43.201 INFO 81710 --- [ main]
org.apache.camel.main.MainSupport :"
+ + " Apache Camel (JBang) 4.24.0-SNAPSHOT is starting\n");
+ out.append("2026-10-17 11:03:43.640 WARN 81710 --- [ main]
sl.yaml.common.YamlDeserializerBase :"
+ + " YAML DSL compact notation detected in:
file:./beans.yaml\n");
+ out.append("2026-10-17 11:03:43.652 ERROR 81710 --- [ main]
org.apache.camel.main.MainSupport :"
+ + " Error starting Camel:
org.apache.camel.FailedToCreateRouteException: " + duplicate + "\n");
+ out.append("org.apache.camel.FailedToCreateRouteException:
").append(duplicate).append('\n');
+ out.append("\tat
org.apache.camel.main.RoutesConfigurer.addDiscoveredRoutes(RoutesConfigurer.java:385)\n");
+ out.append("\tat main.CamelJBang.main(CamelJBang.java:38)\n");
+ out.append("ERROR: ").append(duplicate).append('\n');
+ out.append("org.apache.camel.FailedToCreateRouteException:
").append(duplicate).append('\n');
+ out.append("\tat
org.apache.camel.main.RoutesConfigurer.addDiscoveredRoutes(RoutesConfigurer.java:385)\n");
+ Path output = dir.resolve("camel-launch.log");
+ Files.writeString(output, out.toString());
+
+
assertThat(IntegrationLauncher.failureCause(output)).isEqualTo(duplicate);
+ }
+
+ @Test
+ void failureCauseWithoutTheErrorLineIsTheFirstLoggedException(@TempDir
Path dir) throws Exception {
+ Path output = dir.resolve("camel-launch.log");
+ Files.writeString(output, "2026-10-06 01:12:03.101 INFO 81990 --- [
main] o.a.c.main.MainSupport :"
+ + " Apache Camel (JBang) 4.24.0-SNAPSHOT is
starting\n"
+ + "2026-10-06 01:12:04.552 ERROR 81990 --- [
main] o.a.c.main.MainSupport :"
+ + " Failed to create route:
order-generator\n"
+ +
"org.apache.camel.FailedToCreateRouteException: Failed to create route:
order-generator"
+ + " because of No bean could be found in the
registry for: orderNumber\n"
+ + "\tat
org.apache.camel.impl.engine.AbstractCamelContext.start(AbstractCamelContext.java:1)\n");
+ assertThat(IntegrationLauncher.failureCause(output))
+ .isEqualTo("Failed to create route: order-generator because of
No bean could be found in the registry for:"
+ + " orderNumber");
+
+ // an ERROR record without an exception in the lines after it: the
cause is its message
+ Files.writeString(output, "2026-10-06 01:12:04.552 ERROR 81990 --- [
main] o.a.c.main.MainSupport :"
+ + " Route start failed due to missing bean\n"
+ + "\tat some.Frame(Frame.java:1)\n");
+ assertThat(IntegrationLauncher.failureCause(output)).isEqualTo("Route
start failed due to missing bean");
+
+ // nothing that names an error: no cause, the output says what it says
+ Files.writeString(output, "Downloading dependency
org.example:missing:1.0\n");
+ assertThat(IntegrationLauncher.failureCause(output)).isNull();
+ }
+
/**
* CAMEL-25364: an exception message over several lines is kept, such as
the diagnostics of a Java class that does
* not compile: they are continuation lines in the same block as the
frames, and this output is the only copy.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
index 9fae923e1f89..ee3feeccd0e3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorBeanRefsTest.java
@@ -127,6 +127,46 @@ public class SourceValidatorBeanRefsTest {
.isEmpty();
}
+ @Test
+ void beansDeclaredInTheMavenLayoutAreFound(@TempDir Path dir) throws
IOException {
+ // camel run --source-dir loads the whole directory,
src/main/resources included: a model that put the class in
+ // src/main/java wrote beans.yaml in src/main/resources, and each
reference to the bean was refused as not declared
+ Path pkg =
Files.createDirectories(dir.resolve("src/main/java/camel/example"));
+ Files.writeString(pkg.resolve("OrderNumber.java"), """
+ package camel.example;
+
+ public class OrderNumber {
+ public String next() {
+ return "ORD-1001";
+ }
+ }
+ """);
+ String route = """
+ - from:
+ uri: timer:tick
+ steps:
+ - setHeader:
+ name: orderId
+ simple: "${bean:orderNumber.next}"
+ """;
+ assertThat(SourceValidator.validateYamlBeanRefs(route,
+ SourceValidator.BeanDeclarations.scan(dir,
"order-generator.camel.yaml"), CATALOG))
+ .anyMatch(m -> m.contains("bean 'orderNumber' is not
declared"));
+
+ Path resources =
Files.createDirectories(dir.resolve("src/main/resources"));
+ Files.writeString(resources.resolve("beans.yaml"), """
+ - beans:
+ - name: orderNumber
+ type: camel.example.OrderNumber
+ """);
+ assertThat(SourceValidator.validateYamlBeanRefs(route,
+ SourceValidator.BeanDeclarations.scan(dir,
"order-generator.camel.yaml"), CATALOG))
+ .isEmpty();
+ // the file being validated is left out by its path: its own
declarations come from its new content
+ assertThat(SourceValidator.BeanDeclarations.scan(dir,
"src/main/resources/beans.yaml").names())
+ .doesNotContain("orderNumber");
+ }
+
@Test
void classReferencesPlaceholdersAndUnknownDirectoryAreLeftAlone(@TempDir
Path dir) {
String yaml = ROUTE.replace("aggregationStrategy: myAggregator",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorRoutesTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorRoutesTest.java
index f28bcf158b74..38509988668a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorRoutesTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorRoutesTest.java
@@ -16,6 +16,9 @@
*/
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.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -27,6 +30,7 @@ import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
@@ -110,4 +114,33 @@ class SourceValidatorRoutesTest {
assertThat(notChecked).hasSize(1);
assertThat(notChecked.getString(0)).startsWith("Line 10: not checked:
").contains("options()");
}
+
+ @Test
+ void aRouteIdThatAnotherFileOfTheProjectHasIsReported(@TempDir Path dir)
throws IOException {
+ // a model asked to declare a bean in beans.yaml copied the route
there too: Camel did not start
+ String route = """
+ - route:
+ id: order-generator
+ from:
+ uri: timer:orders
+ steps:
+ - log: "New order"
+ """;
+ Files.writeString(dir.resolve("order-generator.camel.yaml"), route);
+ String beans = """
+ - beans:
+ - name: orderNumber
+ type: "#class:java.lang.StringBuilder"
+ """ + route;
+ assertThat(SourceValidator.validate("beans.yaml", beans, catalog,
null, dir, null, false))
+ .containsExactly("Line 4: route id 'order-generator' is
already the id of a route in"
+ + " order-generator.camel.yaml, and Camel
does not start with two routes of one id (camel"
+ + " run loads every file of the directory):
change the route in order-generator.camel.yaml"
+ + " rather than adding a copy here, or give
this route another id");
+
+ // the file itself, written again, and a route of another id are fine
+ assertThat(SourceValidator.validate("order-generator.camel.yaml",
route, catalog, null, dir, null, false)).isEmpty();
+ assertThat(SourceValidator.validate("beans.yaml", beans.replace("id:
order-generator", "id: other"), catalog, null,
+ dir, null, false)).isEmpty();
+ }
}