This is an automated email from the ASF dual-hosted git repository.
acosentino 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 49781eb8bfce CAMEL-23138 - Camel-jbang-mcp: Add
camel_route_test_scaffold MCP tool (#21727)
49781eb8bfce is described below
commit 49781eb8bfce270aca951ce2923cc2977a98c41c
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Mar 6 10:46:45 2026 +0100
CAMEL-23138 - Camel-jbang-mcp: Add camel_route_test_scaffold MCP tool
(#21727)
* CAMEL-23138 - Camel-jbang-mcp: Add camel_route_test_scaffold MCP tool
Add a new MCP tool that generates JUnit 5 test skeletons from YAML or
XML Camel route definitions. The tool produces ready-to-adapt test
classes with:
- CamelTestSupport for main runtime or @CamelSpringBootTest for
Spring Boot
- Mock endpoints replacing non-trivial producers (kafka, http, etc.)
while preserving log, direct, and seda endpoints
- @RegisterExtension stubs for 20+ test-infra services (Kafka,
Artemis/JMS, MongoDB, PostgreSQL, Cassandra, Elasticsearch, Redis,
RabbitMQ, FTP, Consul, NATS, Pulsar, CouchDB, Infinispan, MinIO,
Solr)
- NotifyBuilder for timer-based routes, template.sendBody() for
direct/seda consumers
- Required Maven test dependency listing
Includes 26 unit tests covering input validation, YAML/XML endpoint
extraction, mock naming, runtime-specific code generation, test-infra
detection with deduplication, and dependency resolution.
Signed-off-by: Andrea Cosentino <[email protected]>
* CAMEL-23138 - Camel-jbang-mcp: Add camel_route_test_scaffold MCP tool -
Docs
Signed-off-by: Andrea Cosentino <[email protected]>
---------
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 49 +-
.../jbang/core/commands/mcp/TestScaffoldTools.java | 726 +++++++++++++++++++++
.../core/commands/mcp/TestScaffoldToolsTest.java | 471 +++++++++++++
3 files changed, 1245 insertions(+), 1 deletion(-)
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 d7d90a1acfc5..03863c00a25e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -24,7 +24,7 @@ By default, the HTTP server is disabled. To enable it, set
`quarkus.http.host-en
== Available Tools
-The server exposes 26 tools organized into ten functional areas, plus 3
prompts that provide structured
+The server exposes 27 tools organized into eleven functional areas, plus 3
prompts that provide structured
multi-step workflows.
=== Catalog Exploration
@@ -88,6 +88,24 @@ multi-step workflows.
from the catalog, and returns structured context.
|===
+=== Test Scaffolding
+
+[cols="1,3",options="header"]
+|===
+| Tool | Description
+
+| `camel_route_test_scaffold`
+| Generates a JUnit 5 test skeleton from a Camel route definition (YAML or
XML). Accepts an optional `format`
+ (`yaml` or `xml`, default `yaml`) and `runtime` (`main` or `spring-boot`,
default `main`). For `main` runtime,
+ the generated test extends `CamelTestSupport`; for `spring-boot`, it uses
`@CamelSpringBootTest` with
+ `@SpringBootTest`. The tool replaces non-trivial producer endpoints with
mock endpoints, generates
+ `@RegisterExtension` stubs for infrastructure components (Kafka,
JMS/Artemis, MongoDB, PostgreSQL, Cassandra,
+ Elasticsearch, Redis, RabbitMQ, FTP, Consul, NATS, Pulsar, CouchDB,
Infinispan, MinIO, Solr), and produces
+ a `NotifyBuilder` pattern for timer-based routes or `template.sendBody()`
for direct/seda consumers. Returns
+ the generated test code, detected components, mock endpoint mappings,
test-infra services, and required Maven
+ test dependencies.
+|===
+
=== Security Analysis
[cols="1,3",options="header"]
@@ -453,6 +471,35 @@ The assistant calls `camel_error_diagnose` which
identifies all three exceptions
`kafka` component, and returns common causes (missing `camel-kafka`
dependency, typo in URI scheme), suggested
fixes (add the dependency, verify the URI), and links to the relevant Camel
documentation.
+=== Generating a Test Skeleton
+
+----
+Generate a JUnit 5 test for this route:
+
+- route:
+ from:
+ uri: kafka:orders
+ steps:
+ - marshal:
+ json: {}
+ - to: mongodb:myDb?collection=orders&operation=insert
+----
+
+The assistant calls `camel_route_test_scaffold` with the route content. It
detects the `kafka` and `mongodb`
+components, generates a test class extending `CamelTestSupport` with
`@RegisterExtension` stubs for both
+`KafkaService` and `MongoDBService`, replaces the `mongodb` producer with a
mock endpoint, and returns the
+generated test code along with the required Maven test dependencies
(`camel-test-junit5`, `camel-mock`,
+`camel-test-infra-kafka`, `camel-test-infra-mongodb`).
+
+For Spring Boot projects, specify the runtime:
+
+----
+Generate a Spring Boot test for this Kafka-to-MongoDB route
+----
+
+The tool generates a test class annotated with `@CamelSpringBootTest` and
`@SpringBootTest`, with
+`CamelContext` and `ProducerTemplate` injected via `@Autowired`.
+
=== Checking Dependency Hygiene
----
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
new file mode 100644
index 000000000000..39e7cace3d70
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
@@ -0,0 +1,726 @@
+/*
+ * 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.mcp;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Tool for generating JUnit 5 test scaffolding for Camel routes.
+ * <p>
+ * Given a YAML or XML route definition, this tool produces a test class
skeleton with CamelTestSupport (or
+ * {@code @CamelSpringBootTest} for Spring Boot), mock endpoints for
producers, and {@code @RegisterExtension} stubs for
+ * infrastructure components like Kafka, JMS, MongoDB, etc.
+ */
+@ApplicationScoped
+public class TestScaffoldTools {
+
+ // YAML endpoint extraction patterns
+ private static final Pattern YAML_FROM_URI
+ = Pattern.compile("from:\\s*\\n\\s+uri:\\s*[\"']?([^\"'\\s#]+)",
Pattern.MULTILINE);
+ private static final Pattern YAML_FROM_INLINE
+ =
Pattern.compile("from:\\s+[\"']?([a-zA-Z][a-zA-Z0-9+.-]*:[^\"'\\s#]+)",
Pattern.MULTILINE);
+ private static final Pattern YAML_TO_URI
+ =
Pattern.compile("-\\s+to:\\s*\\n\\s+uri:\\s*[\"']?([^\"'\\s#]+)",
Pattern.MULTILINE);
+ private static final Pattern YAML_TO_INLINE
+ =
Pattern.compile("-\\s+to:\\s+[\"']?([a-zA-Z][a-zA-Z0-9+.-]*:[^\"'\\s#]+)",
Pattern.MULTILINE);
+ private static final Pattern YAML_TOD_URI
+ =
Pattern.compile("-\\s+toD:\\s*\\n\\s+uri:\\s*[\"']?([^\"'\\s#]+)",
Pattern.MULTILINE);
+ private static final Pattern YAML_TOD_INLINE
+ =
Pattern.compile("-\\s+toD:\\s+[\"']?([a-zA-Z][a-zA-Z0-9+.-]*:[^\"'\\s#]+)",
Pattern.MULTILINE);
+
+ // XML endpoint extraction patterns
+ private static final Pattern XML_FROM =
Pattern.compile("<from\\s+uri=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
+ private static final Pattern XML_TO =
Pattern.compile("<to\\s+uri=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
+ private static final Pattern XML_TOD =
Pattern.compile("<toD\\s+uri=[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
+
+ /** Component schemes that are trivial and should not be replaced with
mocks. */
+ private static final Set<String> TRIVIAL_SCHEMES = Set.of("log", "direct",
"seda", "mock", "controlbus", "stub");
+
+ /** Component schemes that are internally triggered (user can send
messages to them). */
+ private static final Set<String> SENDABLE_SCHEMES = Set.of("direct",
"seda");
+
+ private static final Map<String, TestInfraInfo> TEST_INFRA_MAP =
buildTestInfraMap();
+
+ private final CamelCatalog catalog;
+
+ public TestScaffoldTools() {
+ this.catalog = new DefaultCamelCatalog();
+ }
+
+ /**
+ * Tool to generate a JUnit 5 test skeleton for a Camel route.
+ */
+ @Tool(description = "Generate a JUnit 5 test skeleton for a Camel route. "
+ + "Given a YAML or XML route definition, produces a
test class with "
+ + "CamelTestSupport or @CamelSpringBootTest
boilerplate, "
+ + "mock endpoints for producer endpoints, MockEndpoint
assertions, "
+ + "and @RegisterExtension stubs for infrastructure
components (Kafka, AWS, etc.).")
+ public String camel_route_test_scaffold(
+ @ToolArg(description = "The Camel route definition (YAML or XML)")
String route,
+ @ToolArg(description = "Route format: yaml or xml (default:
yaml)") String format,
+ @ToolArg(description = "Target runtime: main or spring-boot
(default: main)") String runtime) {
+
+ if (route == null || route.isBlank()) {
+ throw new ToolCallException("Route content is required", null);
+ }
+
+ try {
+ String resolvedFormat = resolveFormat(format);
+ String resolvedRuntime = resolveRuntime(runtime);
+
+ // Extract endpoints from route
+ List<String> fromEndpoints = extractFromEndpoints(route,
resolvedFormat);
+ List<String> toEndpoints = extractToEndpoints(route,
resolvedFormat);
+
+ // Collect all unique component schemes
+ List<String> allSchemes = collectSchemes(fromEndpoints,
toEndpoints);
+
+ // Determine which to endpoints need mock replacements
+ List<String> mockEndpoints = toEndpoints.stream()
+ .filter(uri -> {
+ String scheme = extractScheme(uri);
+ return scheme != null &&
!TRIVIAL_SCHEMES.contains(scheme);
+ })
+ .toList();
+
+ // Determine test-infra services needed (deduplicated)
+ List<TestInfraInfo> infraServices =
resolveInfraServices(allSchemes);
+
+ // Generate test code
+ String testCode = "spring-boot".equals(resolvedRuntime)
+ ? generateSpringBootTest(fromEndpoints, mockEndpoints,
infraServices)
+ : generateMainTest(fromEndpoints, mockEndpoints,
infraServices);
+
+ // Build JSON result
+ return buildResult(testCode, resolvedFormat, resolvedRuntime,
+ allSchemes, fromEndpoints, toEndpoints, mockEndpoints,
infraServices);
+
+ } catch (ToolCallException e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new ToolCallException(
+ "Failed to generate test scaffold (" +
e.getClass().getName() + "): " + e.getMessage(), null);
+ }
+ }
+
+ // ---- Endpoint extraction ----
+
+ List<String> extractFromEndpoints(String route, String format) {
+ List<String> endpoints = new ArrayList<>();
+ if ("xml".equals(format)) {
+ addMatches(endpoints, XML_FROM, route);
+ } else {
+ addMatches(endpoints, YAML_FROM_URI, route);
+ if (endpoints.isEmpty()) {
+ addMatches(endpoints, YAML_FROM_INLINE, route);
+ }
+ }
+ return endpoints;
+ }
+
+ List<String> extractToEndpoints(String route, String format) {
+ List<String> endpoints = new ArrayList<>();
+ if ("xml".equals(format)) {
+ addMatches(endpoints, XML_TO, route);
+ addMatches(endpoints, XML_TOD, route);
+ } else {
+ addMatches(endpoints, YAML_TO_URI, route);
+ addMatches(endpoints, YAML_TO_INLINE, route);
+ addMatches(endpoints, YAML_TOD_URI, route);
+ addMatches(endpoints, YAML_TOD_INLINE, route);
+ }
+ return endpoints;
+ }
+
+ private void addMatches(List<String> list, Pattern pattern, String input) {
+ Matcher m = pattern.matcher(input);
+ while (m.find()) {
+ String uri = m.group(1).trim();
+ // Remove trailing quotes if present
+ if (uri.endsWith("\"") || uri.endsWith("'")) {
+ uri = uri.substring(0, uri.length() - 1);
+ }
+ if (!uri.isEmpty()) {
+ list.add(uri);
+ }
+ }
+ }
+
+ // ---- Scheme extraction and mock naming ----
+
+ String extractScheme(String uri) {
+ if (uri == null || uri.isEmpty()) {
+ return null;
+ }
+ // Handle https:// and http:// specially
+ if (uri.startsWith("https://")) {
+ return "https";
+ }
+ if (uri.startsWith("http://")) {
+ return "http";
+ }
+ int idx = uri.indexOf(':');
+ if (idx > 0) {
+ return uri.substring(0, idx);
+ }
+ return null;
+ }
+
+ String toMockName(String uri) {
+ String scheme = extractScheme(uri);
+ if (scheme == null) {
+ return "mock:unknown";
+ }
+
+ // Extract path part (after scheme: and optional //)
+ String rest = uri.substring(scheme.length());
+ if (rest.startsWith("://")) {
+ rest = rest.substring(3);
+ } else if (rest.startsWith(":")) {
+ rest = rest.substring(1);
+ }
+
+ // Remove query parameters
+ int qIdx = rest.indexOf('?');
+ if (qIdx >= 0) {
+ rest = rest.substring(0, qIdx);
+ }
+
+ // Sanitize: replace non-alphanumeric with hyphens, collapse, trim
+ String sanitized = rest.replaceAll("[^a-zA-Z0-9]", "-")
+ .replaceAll("-+", "-")
+ .replaceAll("^-|-$", "");
+
+ if (sanitized.isEmpty()) {
+ return "mock:" + scheme;
+ }
+ return "mock:" + scheme + "-" + sanitized;
+ }
+
+ String toMockVariableName(String mockUri) {
+ // mock:kafka-orders -> mockKafkaOrders
+ String name = mockUri.replace("mock:", "");
+ StringBuilder sb = new StringBuilder("mock");
+ boolean capitalizeNext = true;
+ for (char c : name.toCharArray()) {
+ if (c == '-' || c == '_') {
+ capitalizeNext = true;
+ } else if (capitalizeNext) {
+ sb.append(Character.toUpperCase(c));
+ capitalizeNext = false;
+ } else {
+ sb.append(c);
+ capitalizeNext = false;
+ }
+ }
+ return sb.toString();
+ }
+
+ // ---- Test-infra resolution ----
+
+ private List<TestInfraInfo> resolveInfraServices(List<String> schemes) {
+ Set<TestInfraInfo> seen = new LinkedHashSet<>();
+ for (String scheme : schemes) {
+ TestInfraInfo info = TEST_INFRA_MAP.get(scheme);
+ if (info != null) {
+ seen.add(info);
+ }
+ }
+ return new ArrayList<>(seen);
+ }
+
+ // ---- Code generation ----
+
+ private String generateMainTest(
+ List<String> fromEndpoints, List<String> mockEndpoints,
+ List<TestInfraInfo> infraServices) {
+
+ StringBuilder sb = new StringBuilder();
+
+ // Imports
+ sb.append("import org.apache.camel.RoutesBuilder;\n");
+ sb.append("import org.apache.camel.builder.RouteBuilder;\n");
+ if (!mockEndpoints.isEmpty()) {
+ sb.append("import
org.apache.camel.component.mock.MockEndpoint;\n");
+ }
+ sb.append("import org.apache.camel.test.junit5.CamelTestSupport;\n");
+ if (usesNotifyBuilder(fromEndpoints)) {
+ sb.append("import org.apache.camel.builder.NotifyBuilder;\n");
+ sb.append("import java.util.concurrent.TimeUnit;\n");
+ }
+ sb.append("import org.junit.jupiter.api.Test;\n");
+ for (TestInfraInfo info : infraServices) {
+ sb.append("import
org.junit.jupiter.api.extension.RegisterExtension;\n");
+ break;
+ }
+ for (TestInfraInfo info : infraServices) {
+ sb.append("import
").append(info.packageName).append('.').append(info.serviceClass).append(";\n");
+ sb.append("import
").append(info.packageName).append('.').append(info.factoryClass).append(";\n");
+ }
+ sb.append("\n");
+ sb.append("import static
org.junit.jupiter.api.Assertions.assertTrue;\n");
+ sb.append("\n");
+
+ // Class declaration
+ sb.append("class RouteTest extends CamelTestSupport {\n\n");
+
+ // @RegisterExtension fields
+ for (TestInfraInfo info : infraServices) {
+ String fieldName =
Character.toLowerCase(info.serviceClass.charAt(0)) +
info.serviceClass.substring(1);
+ sb.append(" @RegisterExtension\n");
+ sb.append(" static ").append(info.serviceClass).append('
').append(fieldName);
+ sb.append(" =
").append(info.factoryClass).append(".createService();\n\n");
+ }
+
+ // createRouteBuilder
+ sb.append(" @Override\n");
+ sb.append(" protected RoutesBuilder createRouteBuilder() {\n");
+ sb.append(" return new RouteBuilder() {\n");
+ sb.append(" @Override\n");
+ sb.append(" public void configure() {\n");
+
+ String fromUri = fromEndpoints.isEmpty() ? "direct:start" :
fromEndpoints.get(0);
+ String fromScheme = extractScheme(fromUri);
+ boolean isSendable = fromScheme != null &&
SENDABLE_SCHEMES.contains(fromScheme);
+ boolean isTimer = "timer".equals(fromScheme);
+
+ if (!isSendable && !isTimer && !fromEndpoints.isEmpty()) {
+ sb.append(" // TODO: Replace with actual consumer
URI or use direct:start for unit testing\n");
+ }
+ sb.append("
from(\"").append(escapeJava(fromUri)).append("\")\n");
+
+ if (mockEndpoints.isEmpty()) {
+ sb.append(" .log(\"Route executed\");\n");
+ } else {
+ for (int i = 0; i < mockEndpoints.size(); i++) {
+ String mockName = toMockName(mockEndpoints.get(i));
+ String originalUri = mockEndpoints.get(i);
+ sb.append("
.to(\"").append(escapeJava(mockName)).append("\")");
+ sb.append(" // replaces
").append(extractScheme(originalUri)).append(":...");
+ if (i == mockEndpoints.size() - 1) {
+ sb.append(";\n");
+ } else {
+ sb.append("\n");
+ }
+ }
+ }
+
+ sb.append(" }\n");
+ sb.append(" };\n");
+ sb.append(" }\n\n");
+
+ // Test method
+ sb.append(" @Test\n");
+ sb.append(" void testRoute() throws Exception {\n");
+
+ // Mock endpoint declarations
+ for (String mockUri : mockEndpoints) {
+ String mockName = toMockName(mockUri);
+ String varName = toMockVariableName(mockName);
+ sb.append(" MockEndpoint ").append(varName).append(" =
getMockEndpoint(\"");
+ sb.append(escapeJava(mockName)).append("\");\n");
+ sb.append("
").append(varName).append(".expectedMinimumMessageCount(1);\n\n");
+ }
+
+ // Send or wait
+ if (isTimer) {
+ sb.append(" // Timer route fires automatically; use
NotifyBuilder to wait\n");
+ sb.append(" NotifyBuilder notify = new
NotifyBuilder(context).whenDone(1).create();\n");
+ sb.append(
+ " assertTrue(notify.matches(10, TimeUnit.SECONDS),
\"Route should complete within timeout\");\n\n");
+ } else if (isSendable) {
+ sb.append("
template.sendBody(\"").append(escapeJava(fromUri)).append("\", \"test
message\");\n\n");
+ } else {
+ sb.append(" // TODO: Send a test message to trigger the
route\n");
+ sb.append(" //
template.sendBody(\"").append(escapeJava(fromUri)).append("\", \"test
message\");\n\n");
+ }
+
+ // Assert
+ if (!mockEndpoints.isEmpty()) {
+ sb.append(" MockEndpoint.assertIsSatisfied(context);\n");
+ }
+
+ sb.append(" }\n");
+ sb.append("}\n");
+
+ return sb.toString();
+ }
+
+ private String generateSpringBootTest(
+ List<String> fromEndpoints, List<String> mockEndpoints,
+ List<TestInfraInfo> infraServices) {
+
+ StringBuilder sb = new StringBuilder();
+
+ // Imports
+ sb.append("import org.apache.camel.CamelContext;\n");
+ sb.append("import org.apache.camel.ProducerTemplate;\n");
+ if (!mockEndpoints.isEmpty()) {
+ sb.append("import
org.apache.camel.component.mock.MockEndpoint;\n");
+ }
+ sb.append("import
org.apache.camel.test.spring.junit5.CamelSpringBootTest;\n");
+ if (usesNotifyBuilder(fromEndpoints)) {
+ sb.append("import org.apache.camel.builder.NotifyBuilder;\n");
+ sb.append("import java.util.concurrent.TimeUnit;\n");
+ }
+ sb.append("import org.junit.jupiter.api.Test;\n");
+ sb.append("import
org.springframework.beans.factory.annotation.Autowired;\n");
+ sb.append("import
org.springframework.boot.test.context.SpringBootTest;\n");
+ for (TestInfraInfo info : infraServices) {
+ sb.append("import
org.junit.jupiter.api.extension.RegisterExtension;\n");
+ break;
+ }
+ for (TestInfraInfo info : infraServices) {
+ sb.append("import
").append(info.packageName).append('.').append(info.serviceClass).append(";\n");
+ sb.append("import
").append(info.packageName).append('.').append(info.factoryClass).append(";\n");
+ }
+ sb.append("\n");
+ sb.append("import static
org.junit.jupiter.api.Assertions.assertTrue;\n");
+ sb.append("\n");
+
+ // Class declaration
+ sb.append("@CamelSpringBootTest\n");
+ sb.append("@SpringBootTest\n");
+ sb.append("class RouteTest {\n\n");
+
+ // Injected fields
+ sb.append(" @Autowired\n");
+ sb.append(" private CamelContext context;\n\n");
+ sb.append(" @Autowired\n");
+ sb.append(" private ProducerTemplate template;\n\n");
+
+ // @RegisterExtension fields
+ for (TestInfraInfo info : infraServices) {
+ String fieldName =
Character.toLowerCase(info.serviceClass.charAt(0)) +
info.serviceClass.substring(1);
+ sb.append(" @RegisterExtension\n");
+ sb.append(" static ").append(info.serviceClass).append('
').append(fieldName);
+ sb.append(" =
").append(info.factoryClass).append(".createService();\n\n");
+ }
+
+ // Test method
+ sb.append(" @Test\n");
+ sb.append(" void testRoute() throws Exception {\n");
+
+ // Mock endpoint declarations
+ for (String mockUri : mockEndpoints) {
+ String mockName = toMockName(mockUri);
+ String varName = toMockVariableName(mockName);
+ sb.append(" // TODO: Use AdviceWith or
@MockEndpointsAndSkip to intercept endpoints\n");
+ sb.append(" MockEndpoint ").append(varName);
+ sb.append(" =
context.getEndpoint(\"").append(escapeJava(mockName));
+ sb.append("\", MockEndpoint.class);\n");
+ sb.append("
").append(varName).append(".expectedMinimumMessageCount(1);\n\n");
+ }
+
+ // Send or wait
+ String fromUri = fromEndpoints.isEmpty() ? "direct:start" :
fromEndpoints.get(0);
+ String fromScheme = extractScheme(fromUri);
+ boolean isSendable = fromScheme != null &&
SENDABLE_SCHEMES.contains(fromScheme);
+ boolean isTimer = "timer".equals(fromScheme);
+
+ if (isTimer) {
+ sb.append(" // Timer route fires automatically; use
NotifyBuilder to wait\n");
+ sb.append(" NotifyBuilder notify = new
NotifyBuilder(context).whenDone(1).create();\n");
+ sb.append(
+ " assertTrue(notify.matches(10, TimeUnit.SECONDS),
\"Route should complete within timeout\");\n\n");
+ } else if (isSendable) {
+ sb.append("
template.sendBody(\"").append(escapeJava(fromUri)).append("\", \"test
message\");\n\n");
+ } else {
+ sb.append(" // TODO: Send a test message to trigger the
route\n");
+ sb.append(" // template.sendBody(\"direct:start\", \"test
message\");\n\n");
+ }
+
+ // Assert
+ if (!mockEndpoints.isEmpty()) {
+ sb.append(" MockEndpoint.assertIsSatisfied(context);\n");
+ }
+
+ sb.append(" }\n");
+ sb.append("}\n");
+
+ return sb.toString();
+ }
+
+ // ---- Helpers ----
+
+ private boolean usesNotifyBuilder(List<String> fromEndpoints) {
+ if (fromEndpoints.isEmpty()) {
+ return false;
+ }
+ String scheme = extractScheme(fromEndpoints.get(0));
+ return "timer".equals(scheme);
+ }
+
+ private List<String> collectSchemes(List<String> fromEndpoints,
List<String> toEndpoints) {
+ List<String> schemes = new ArrayList<>();
+ for (String uri : fromEndpoints) {
+ String s = extractScheme(uri);
+ if (s != null && !schemes.contains(s)) {
+ schemes.add(s);
+ }
+ }
+ for (String uri : toEndpoints) {
+ String s = extractScheme(uri);
+ if (s != null && !schemes.contains(s)) {
+ schemes.add(s);
+ }
+ }
+ return schemes;
+ }
+
+ private String resolveFormat(String format) {
+ return format != null && !format.isBlank() ? format.toLowerCase() :
"yaml";
+ }
+
+ private String resolveRuntime(String runtime) {
+ return runtime != null && !runtime.isBlank() ? runtime.toLowerCase() :
"main";
+ }
+
+ private String escapeJava(String s) {
+ return s.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ // ---- JSON result builder ----
+
+ private String buildResult(
+ String testCode, String format, String runtime,
+ List<String> allSchemes, List<String> fromEndpoints, List<String>
toEndpoints,
+ List<String> mockEndpoints, List<TestInfraInfo> infraServices) {
+
+ JsonObject result = new JsonObject();
+ result.put("testCode", testCode);
+ result.put("runtime", runtime);
+ result.put("format", format);
+
+ // Detected components
+ JsonArray componentsJson = new JsonArray();
+ for (String scheme : allSchemes) {
+ JsonObject comp = new JsonObject();
+ comp.put("scheme", scheme);
+ ComponentModel model = catalog.componentModel(scheme);
+ if (model != null) {
+ comp.put("title", model.getTitle());
+ comp.put("producerOnly", model.isProducerOnly());
+ comp.put("consumerOnly", model.isConsumerOnly());
+ }
+ componentsJson.add(comp);
+ }
+ result.put("detectedComponents", componentsJson);
+
+ // Mock endpoints
+ JsonArray mocksJson = new JsonArray();
+ for (String uri : mockEndpoints) {
+ JsonObject mockObj = new JsonObject();
+ mockObj.put("originalUri", uri);
+ mockObj.put("mockUri", toMockName(uri));
+ mocksJson.add(mockObj);
+ }
+ result.put("mockEndpoints", mocksJson);
+
+ // Test-infra services
+ JsonArray infraJson = new JsonArray();
+ for (TestInfraInfo info : infraServices) {
+ JsonObject infraObj = new JsonObject();
+ infraObj.put("service", info.serviceClass);
+ infraObj.put("factory", info.factoryClass);
+ infraObj.put("artifactId", info.artifactId);
+ infraObj.put("package", info.packageName);
+ infraJson.add(infraObj);
+ }
+ result.put("testInfraServices", infraJson);
+
+ // Maven dependencies
+ JsonArray depsJson = new JsonArray();
+ addDependency(depsJson, "camel-test-junit5");
+ if (!mockEndpoints.isEmpty()) {
+ addDependency(depsJson, "camel-mock");
+ }
+ for (TestInfraInfo info : infraServices) {
+ addDependency(depsJson, info.artifactId);
+ }
+ if ("spring-boot".equals(runtime)) {
+ addDependency(depsJson, "camel-test-spring-junit5");
+ }
+ result.put("mavenDependencies", depsJson);
+
+ // Summary
+ JsonObject summary = new JsonObject();
+ summary.put("componentCount", allSchemes.size());
+ summary.put("mockEndpointCount", mockEndpoints.size());
+ summary.put("testInfraServiceCount", infraServices.size());
+ summary.put("fromEndpointCount", fromEndpoints.size());
+ summary.put("toEndpointCount", toEndpoints.size());
+ result.put("summary", summary);
+
+ return result.toJson();
+ }
+
+ private void addDependency(JsonArray array, String artifactId) {
+ JsonObject dep = new JsonObject();
+ dep.put("groupId", "org.apache.camel");
+ dep.put("artifactId", artifactId);
+ dep.put("scope", "test");
+ array.add(dep);
+ }
+
+ // ---- Test-infra mapping ----
+
+ private static Map<String, TestInfraInfo> buildTestInfraMap() {
+ Map<String, TestInfraInfo> map = new LinkedHashMap<>();
+
+ TestInfraInfo kafka = new TestInfraInfo(
+ "KafkaService", "KafkaServiceFactory",
+ "camel-test-infra-kafka",
"org.apache.camel.test.infra.kafka.services");
+ map.put("kafka", kafka);
+
+ TestInfraInfo artemis = new TestInfraInfo(
+ "ArtemisService", "ArtemisServiceFactory",
+ "camel-test-infra-artemis",
"org.apache.camel.test.infra.artemis.services");
+ map.put("jms", artemis);
+ map.put("activemq", artemis);
+ map.put("sjms", artemis);
+ map.put("sjms2", artemis);
+ map.put("amqp", artemis);
+
+ TestInfraInfo mongodb = new TestInfraInfo(
+ "MongoDBService", "MongoDBServiceFactory",
+ "camel-test-infra-mongodb",
"org.apache.camel.test.infra.mongodb.services");
+ map.put("mongodb", mongodb);
+
+ TestInfraInfo postgres = new TestInfraInfo(
+ "PostgresService", "PostgresServiceFactory",
+ "camel-test-infra-postgres",
"org.apache.camel.test.infra.postgres.services");
+ map.put("sql", postgres);
+ map.put("jdbc", postgres);
+
+ TestInfraInfo cassandra = new TestInfraInfo(
+ "CassandraService", "CassandraServiceFactory",
+ "camel-test-infra-cassandra",
"org.apache.camel.test.infra.cassandra.services");
+ map.put("cql", cassandra);
+
+ TestInfraInfo elasticsearch = new TestInfraInfo(
+ "ElasticSearchService", "ElasticSearchServiceFactory",
+ "camel-test-infra-elasticsearch",
"org.apache.camel.test.infra.elasticsearch.services");
+ map.put("elasticsearch", elasticsearch);
+ map.put("elasticsearch-rest", elasticsearch);
+
+ TestInfraInfo redis = new TestInfraInfo(
+ "RedisService", "RedisServiceFactory",
+ "camel-test-infra-redis",
"org.apache.camel.test.infra.redis.services");
+ map.put("spring-redis", redis);
+
+ TestInfraInfo rabbitmq = new TestInfraInfo(
+ "RabbitMQService", "RabbitMQServiceFactory",
+ "camel-test-infra-rabbitmq",
"org.apache.camel.test.infra.rabbitmq.services");
+ map.put("rabbitmq", rabbitmq);
+
+ TestInfraInfo ftp = new TestInfraInfo(
+ "FtpService", "FtpServiceFactory",
+ "camel-test-infra-ftp",
"org.apache.camel.test.infra.ftp.services");
+ map.put("ftp", ftp);
+ map.put("sftp", ftp);
+ map.put("ftps", ftp);
+
+ TestInfraInfo consul = new TestInfraInfo(
+ "ConsulService", "ConsulServiceFactory",
+ "camel-test-infra-consul",
"org.apache.camel.test.infra.consul.services");
+ map.put("consul", consul);
+
+ TestInfraInfo nats = new TestInfraInfo(
+ "NatsService", "NatsServiceFactory",
+ "camel-test-infra-nats",
"org.apache.camel.test.infra.nats.services");
+ map.put("nats", nats);
+
+ TestInfraInfo pulsar = new TestInfraInfo(
+ "PulsarService", "PulsarServiceFactory",
+ "camel-test-infra-pulsar",
"org.apache.camel.test.infra.pulsar.services");
+ map.put("pulsar", pulsar);
+
+ TestInfraInfo couchdb = new TestInfraInfo(
+ "CouchDbService", "CouchDbServiceFactory",
+ "camel-test-infra-couchdb",
"org.apache.camel.test.infra.couchdb.services");
+ map.put("couchdb", couchdb);
+
+ TestInfraInfo infinispan = new TestInfraInfo(
+ "InfinispanService", "InfinispanServiceFactory",
+ "camel-test-infra-infinispan",
"org.apache.camel.test.infra.infinispan.services");
+ map.put("infinispan", infinispan);
+
+ TestInfraInfo minio = new TestInfraInfo(
+ "MinioService", "MinioServiceFactory",
+ "camel-test-infra-minio",
"org.apache.camel.test.infra.minio.services");
+ map.put("minio", minio);
+
+ TestInfraInfo solr = new TestInfraInfo(
+ "SolrService", "SolrServiceFactory",
+ "camel-test-infra-solr",
"org.apache.camel.test.infra.solr.services");
+ map.put("solr", solr);
+
+ return map;
+ }
+
+ /**
+ * Holds test-infra service information for a component.
+ */
+ static final class TestInfraInfo {
+ final String serviceClass;
+ final String factoryClass;
+ final String artifactId;
+ final String packageName;
+
+ TestInfraInfo(String serviceClass, String factoryClass, String
artifactId, String packageName) {
+ this.serviceClass = serviceClass;
+ this.factoryClass = factoryClass;
+ this.artifactId = artifactId;
+ this.packageName = packageName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof TestInfraInfo that)) {
+ return false;
+ }
+ return artifactId.equals(that.artifactId);
+ }
+
+ @Override
+ public int hashCode() {
+ return artifactId.hashCode();
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldToolsTest.java
new file mode 100644
index 000000000000..1f680d47aaf9
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldToolsTest.java
@@ -0,0 +1,471 @@
+/*
+ * 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.mcp;
+
+import java.util.List;
+
+import io.quarkiverse.mcp.server.ToolCallException;
+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 static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class TestScaffoldToolsTest {
+
+ private final TestScaffoldTools tools = new TestScaffoldTools();
+
+ // ---- Input validation ----
+
+ @Test
+ void nullRouteThrows() {
+ assertThatThrownBy(() -> tools.camel_route_test_scaffold(null, null,
null))
+ .isInstanceOf(ToolCallException.class)
+ .hasMessageContaining("required");
+ }
+
+ @Test
+ void blankRouteThrows() {
+ assertThatThrownBy(() -> tools.camel_route_test_scaffold(" ",
"yaml", "main"))
+ .isInstanceOf(ToolCallException.class)
+ .hasMessageContaining("required");
+ }
+
+ // ---- YAML endpoint extraction ----
+
+ @Test
+ void extractsFromEndpointFromYaml() {
+ String route = """
+ - route:
+ from:
+ uri: timer:tick?period=5000
+ steps:
+ - to: log:done
+ """;
+
+ List<String> from = tools.extractFromEndpoints(route, "yaml");
+ assertThat(from).containsExactly("timer:tick?period=5000");
+ }
+
+ @Test
+ void extractsToEndpointsFromYaml() {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: kafka:orders?brokers=localhost:9092
+ - to: log:done
+ """;
+
+ List<String> to = tools.extractToEndpoints(route, "yaml");
+ assertThat(to).containsExactly("kafka:orders?brokers=localhost:9092",
"log:done");
+ }
+
+ @Test
+ void extractsQuotedYamlEndpoints() {
+ String route = """
+ - route:
+ from:
+ uri: "direct:start"
+ steps:
+ - to: "kafka:orders?brokers=localhost:9092"
+ """;
+
+ List<String> from = tools.extractFromEndpoints(route, "yaml");
+ assertThat(from).hasSize(1);
+ assertThat(from.get(0)).startsWith("direct:start");
+
+ List<String> to = tools.extractToEndpoints(route, "yaml");
+ assertThat(to).hasSize(1);
+ assertThat(to.get(0)).startsWith("kafka:orders");
+ }
+
+ // ---- XML endpoint extraction ----
+
+ @Test
+ void extractsFromEndpointFromXml() {
+ String route = """
+ <route>
+ <from uri="timer:tick?period=5000"/>
+ <to uri="log:done"/>
+ </route>
+ """;
+
+ List<String> from = tools.extractFromEndpoints(route, "xml");
+ assertThat(from).containsExactly("timer:tick?period=5000");
+ }
+
+ @Test
+ void extractsToEndpointsFromXml() {
+ String route = """
+ <route>
+ <from uri="direct:start"/>
+ <to uri="kafka:orders?brokers=localhost:9092"/>
+ <to uri="log:done"/>
+ </route>
+ """;
+
+ List<String> to = tools.extractToEndpoints(route, "xml");
+ assertThat(to).containsExactly("kafka:orders?brokers=localhost:9092",
"log:done");
+ }
+
+ @Test
+ void extractsToDEndpointFromXml() {
+ String route = """
+ <route>
+ <from uri="direct:start"/>
+ <toD uri="http:api.example.com/notify"/>
+ </route>
+ """;
+
+ List<String> to = tools.extractToEndpoints(route, "xml");
+ assertThat(to).containsExactly("http:api.example.com/notify");
+ }
+
+ // ---- Scheme extraction ----
+
+ @Test
+ void extractsSchemeFromUri() {
+
assertThat(tools.extractScheme("kafka:orders?brokers=localhost")).isEqualTo("kafka");
+ assertThat(tools.extractScheme("timer:tick")).isEqualTo("timer");
+ assertThat(tools.extractScheme("direct:start")).isEqualTo("direct");
+
assertThat(tools.extractScheme("https://api.example.com")).isEqualTo("https");
+
assertThat(tools.extractScheme("http://api.example.com")).isEqualTo("http");
+ }
+
+ @Test
+ void extractsSchemeReturnsNullForInvalid() {
+ assertThat(tools.extractScheme(null)).isNull();
+ assertThat(tools.extractScheme("")).isNull();
+ assertThat(tools.extractScheme("noscheme")).isNull();
+ }
+
+ // ---- Mock naming ----
+
+ @Test
+ void generatesMockNames() {
+
assertThat(tools.toMockName("kafka:orders?brokers=localhost:9092")).isEqualTo("mock:kafka-orders");
+
assertThat(tools.toMockName("http://api.example.com/notify")).isEqualTo("mock:http-api-example-com-notify");
+
assertThat(tools.toMockName("timer:tick")).isEqualTo("mock:timer-tick");
+ }
+
+ @Test
+ void generatesMockVariableNames() {
+
assertThat(tools.toMockVariableName("mock:kafka-orders")).isEqualTo("mockKafkaOrders");
+
assertThat(tools.toMockVariableName("mock:http-api")).isEqualTo("mockHttpApi");
+ }
+
+ // ---- Main runtime test generation ----
+
+ @Test
+ void generatesMainTestWithDirectFrom() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: kafka:orders?brokers=localhost:9092
+ - to: log:done
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ assertThat(result.getString("runtime")).isEqualTo("main");
+
+ String testCode = result.getString("testCode");
+ assertThat(testCode).contains("extends CamelTestSupport");
+ assertThat(testCode).contains("createRouteBuilder");
+ assertThat(testCode).contains("mock:kafka-orders");
+ assertThat(testCode).contains("template.sendBody(\"direct:start\"");
+
assertThat(testCode).contains("MockEndpoint.assertIsSatisfied(context)");
+ }
+
+ @Test
+ void generatesMainTestWithTimerFrom() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: timer:tick?period=5000
+ steps:
+ - to: kafka:orders
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ String testCode = result.getString("testCode");
+ assertThat(testCode).contains("NotifyBuilder");
+ assertThat(testCode).contains("whenDone(1)");
+ assertThat(testCode).doesNotContain("template.sendBody");
+ }
+
+ @Test
+ void doesNotMockLogEndpoint() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: log:done
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray mocks = result.getCollection("mockEndpoints");
+ assertThat(mocks).isEmpty();
+ }
+
+ @Test
+ void doesNotMockDirectOrSedaEndpoints() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: direct:next
+ - to: seda:async
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray mocks = result.getCollection("mockEndpoints");
+ assertThat(mocks).isEmpty();
+ }
+
+ // ---- Spring Boot runtime test generation ----
+
+ @Test
+ void generatesSpringBootTest() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: kafka:orders
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml",
"spring-boot");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ assertThat(result.getString("runtime")).isEqualTo("spring-boot");
+
+ String testCode = result.getString("testCode");
+ assertThat(testCode).contains("@CamelSpringBootTest");
+ assertThat(testCode).contains("@SpringBootTest");
+ assertThat(testCode).contains("@Autowired");
+ assertThat(testCode).doesNotContain("extends CamelTestSupport");
+ }
+
+ // ---- Test-infra detection ----
+
+ @Test
+ void detectsKafkaTestInfra() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: kafka:orders?brokers=localhost:9092
+ steps:
+ - to: log:done
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray infra = result.getCollection("testInfraServices");
+ assertThat(infra).isNotEmpty();
+ assertThat(infra.getMap(0).get("service")).isEqualTo("KafkaService");
+
assertThat(infra.getMap(0).get("factory")).isEqualTo("KafkaServiceFactory");
+
assertThat(infra.getMap(0).get("artifactId")).isEqualTo("camel-test-infra-kafka");
+
+ String testCode = result.getString("testCode");
+ assertThat(testCode).contains("@RegisterExtension");
+ assertThat(testCode).contains("KafkaServiceFactory.createService()");
+ }
+
+ @Test
+ void detectsJmsTestInfra() throws Exception {
+ String route = """
+ <route>
+ <from uri="jms:queue:orders"/>
+ <to uri="log:done"/>
+ </route>
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "xml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray infra = result.getCollection("testInfraServices");
+ assertThat(infra).isNotEmpty();
+ assertThat(infra.getMap(0).get("service")).isEqualTo("ArtemisService");
+
assertThat(infra.getMap(0).get("artifactId")).isEqualTo("camel-test-infra-artemis");
+ }
+
+ @Test
+ void detectsMultipleTestInfraServices() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: kafka:orders
+ steps:
+ - to: mongodb:myDb?collection=orders
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray infra = result.getCollection("testInfraServices");
+ assertThat(infra.size()).isEqualTo(2);
+
+ List<String> services = infra.stream()
+ .map(i -> ((JsonObject) i).getString("service"))
+ .toList();
+ assertThat(services).contains("KafkaService", "MongoDBService");
+ }
+
+ @Test
+ void deduplicatesTestInfraForSameService() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: jms:queue:input
+ steps:
+ - to: activemq:queue:output
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray infra = result.getCollection("testInfraServices");
+ // jms and activemq both map to ArtemisService, should be deduplicated
+ assertThat(infra.size()).isEqualTo(1);
+ assertThat(infra.getMap(0).get("service")).isEqualTo("ArtemisService");
+ }
+
+ // ---- Maven dependencies ----
+
+ @Test
+ void includesTestDependencies() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: kafka:orders
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray deps = result.getCollection("mavenDependencies");
+ List<String> artifactIds = deps.stream()
+ .map(d -> ((JsonObject) d).getString("artifactId"))
+ .toList();
+ assertThat(artifactIds).contains("camel-test-junit5", "camel-mock",
"camel-test-infra-kafka");
+ }
+
+ @Test
+ void includesSpringBootTestDependency() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: kafka:orders
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml",
"spring-boot");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ JsonArray deps = result.getCollection("mavenDependencies");
+ List<String> artifactIds = deps.stream()
+ .map(d -> ((JsonObject) d).getString("artifactId"))
+ .toList();
+ assertThat(artifactIds).contains("camel-test-spring-junit5");
+ }
+
+ // ---- Summary ----
+
+ @Test
+ void resultContainsSummary() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: kafka:orders
+ - to: log:done
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "yaml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+ JsonObject summary = result.getMap("summary");
+
+ assertThat(summary).isNotNull();
+ assertThat(summary.getInteger("fromEndpointCount")).isEqualTo(1);
+ assertThat(summary.getInteger("toEndpointCount")).isEqualTo(2);
+ assertThat(summary.getInteger("mockEndpointCount")).isEqualTo(1); //
kafka only, log is trivial
+ assertThat(summary.getInteger("testInfraServiceCount")).isEqualTo(1);
// kafka
+ }
+
+ // ---- Default format and runtime ----
+
+ @Test
+ void defaultsToYamlAndMain() throws Exception {
+ String route = """
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - to: log:done
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, null, null);
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ assertThat(result.getString("format")).isEqualTo("yaml");
+ assertThat(result.getString("runtime")).isEqualTo("main");
+ }
+
+ // ---- XML full test ----
+
+ @Test
+ void generatesTestFromXmlRoute() throws Exception {
+ String route = """
+ <route>
+ <from uri="timer:tick?period=5000"/>
+ <to uri="kafka:orders?brokers=localhost:9092"/>
+ <to uri="log:done"/>
+ </route>
+ """;
+
+ String json = tools.camel_route_test_scaffold(route, "xml", "main");
+ JsonObject result = (JsonObject) Jsoner.deserialize(json);
+
+ String testCode = result.getString("testCode");
+ assertThat(testCode).contains("extends CamelTestSupport");
+ assertThat(testCode).contains("mock:kafka-orders");
+ assertThat(testCode).contains("NotifyBuilder"); // timer from
+ assertThat(testCode).contains("@RegisterExtension");
+ assertThat(testCode).contains("KafkaServiceFactory");
+ }
+}