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 af6bef4113d8 CAMEL-26493: camel-jbang - AI tools: a Citrus test is
checked as YAML, not as a route, and the examples show their Citrus tests
(#28872)
af6bef4113d8 is described below
commit af6bef4113d853d5595b9ffe9fbbb45005cd97ba
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:25:41 2026 +0100
CAMEL-26493: camel-jbang - AI tools: a Citrus test is checked as YAML, not
as a route, and the examples show their Citrus tests (#28872)
* CAMEL-26493: camel-jbang - AI tools: a Citrus test is checked as YAML,
not as a route, and the examples show their Citrus tests
The local-model benchmark example test-citrus-orders asks for a Citrus test
in
test/orders.citrus.it.yaml, run with camel test run. It showed two gaps.
camel_write_file, camel_edit_file and camel_validate_source checked the
test as a
Camel YAML route. The first test of the model was refused, as a Citrus
variable
(${orderId}) was read as a Simple function. The next one was written with
the
problem "a Camel YAML file is a list of entries", and the answer said that
the
running integration reloaded the file. The model then made the test a list,
and
then a route.
camel_catalog_examples had no Citrus test to show: a search for citrus
found no
example, and the files of an example leave out test/.
Now:
- A Citrus test (name.citrus.yaml, name.citrus.it.yaml, any
name.citrus.*.yaml) is
checked for its YAML syntax only: a parse error refuses the write, with
its line.
The Camel YAML repairs and the route notes are not applied to it.
- The write of a Citrus test does not wait for a reload and does not report
one.
camel run --dev --source-dir reloads the routes when the test changes,
but it
loads no route from the test. The answer says to run the test with camel
test run.
- The example tools list the Citrus test of an example
(test/<name>.citrus.it.yaml)
with its files, and read it. The tests of the bundled examples are
bundled, and
the sync of the example catalog fetches them. camel run --example still
runs only
the files of the catalog.
- A search for citrus finds the examples with a Citrus test. A filter finds
the
examples that have every word of it, so "csv-to-json test" finds that
example.
- The MCP server instructions say where to find a sample Citrus test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Y4dDi5iQNgtHrDdz2NpwF
* CAMEL-26493: camel-jbang - ExampleRoutesLoadTest leaves out the Citrus
tests of the examples, they are not routes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
dsl/camel-jbang/camel-jbang-core/pom.xml | 5 +-
.../dsl/jbang/core/commands/ai/AuthoringTools.java | 47 +++--
.../jbang/core/commands/ai/SourceValidator.java | 59 +++++-
.../dsl/jbang/core/commands/ai/ToolRegistry.java | 29 ++-
.../camel/dsl/jbang/core/common/ExampleHelper.java | 60 +++---
.../test/file-processing.citrus.it.yaml | 26 +++
.../http-client/test/http-client.citrus.it.yaml | 21 ++
.../stock-api/test/stock-api.citrus.it.yaml | 49 +++++
.../test/openapi-client.citrus.it.yaml | 39 ++++
.../test/openapi-server.citrus.it.yaml | 76 +++++++
.../test/rest-code-first.citrus.it.yaml | 95 +++++++++
.../test/circuit-breaker.citrus.it.yaml | 26 +++
.../test/error-handling.citrus.it.yaml | 36 ++++
.../rest-api/test/rest-api.citrus.it.yaml | 40 ++++
.../quick-start/routes/test/routes.citrus.it.yaml | 19 ++
.../splitter/test/splitter.citrus.it.yaml | 19 ++
.../timer-log/test/timer-log.citrus.it.yaml | 16 ++
.../aggregator/test/aggregator.citrus.it.yaml | 25 +++
.../test/content-based-router.citrus.it.yaml | 30 +++
.../test/dynamic-routing.citrus.it.yaml | 35 ++++
.../test/enrich-and-dedupe.citrus.it.yaml | 34 ++++
.../test/filter-and-multicast.citrus.it.yaml | 30 +++
.../order-lines/test/order-lines.citrus.it.yaml | 25 +++
.../test/nightly-report.citrus.it.yaml | 16 ++
.../test/order-generator.citrus.it.yaml | 24 +++
.../test/properties-and-profiles.citrus.it.yaml | 16 ++
.../csv-to-json/test/csv-to-json.citrus.it.yaml | 21 ++
.../data-mapping/test/data-mapping.citrus.it.yaml | 24 +++
.../transform/groovy/test/groovy.citrus.it.yaml | 24 +++
.../test/json-transform.citrus.it.yaml | 23 +++
.../xml-to-json/test/xml-to-json.citrus.it.yaml | 16 ++
.../transform/xslt/test/xslt.citrus.it.yaml | 23 +++
.../core/commands/ai/AuthoringToolsCitrusTest.java | 221 +++++++++++++++++++++
.../core/commands/ai/ExampleToolsRegistryTest.java | 54 ++++-
.../dsl/jbang/core/common/ExampleHelperTest.java | 29 +++
.../jbang/core/common/ExampleRoutesLoadTest.java | 6 +-
.../dsl/jbang/core/commands/mcp/ExampleTools.java | 9 +-
.../core/commands/mcp/McpServerInstructions.java | 2 +
.../jbang/core/commands/mcp/ExampleToolsTest.java | 16 ++
.../commands/mcp/McpServerInstructionsTest.java | 7 +
40 files changed, 1321 insertions(+), 51 deletions(-)
diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml
b/dsl/camel-jbang/camel-jbang-core/pom.xml
index b1a5b666c0d2..d87c1d528a73 100644
--- a/dsl/camel-jbang/camel-jbang-core/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-core/pom.xml
@@ -527,7 +527,10 @@
}
files.findAll { it.directory
}.each { it.deleteDir() }
catalog.findAll { it.bundled
}.each { ex ->
- ex.files.each { f ->
+ // the files it runs with, and
its Citrus test (test/<name>.citrus.it.yaml), which the catalog does not list
+ def leaf =
ex.name.substring(ex.name.lastIndexOf('/') + 1)
+ def fetched = ex.files +
(ex.hasCitrusTests ? ['test/' + leaf + '.citrus.it.yaml'] : [])
+ fetched.each { f ->
def target = new
File(dest, ex.name + '/' + f)
target.parentFile.mkdirs()
try {
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 8bf33e1dd72d..d62d000be043 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
@@ -660,12 +660,25 @@ public final class AuthoringTools {
result.put("notChecked", notChecked);
}
}
- result.put("message", errors.isEmpty()
- ? "The source is valid"
- : errors.size() + " problem(s) found; fix them before writing
the file");
+ if (!errors.isEmpty()) {
+ result.put("message", errors.size() + " problem(s) found; fix them
before writing the file");
+ } else if (SourceValidator.isCitrusTest(file)) {
+ result.put("message", "The YAML is valid. " +
citrusTestNote(file));
+ } else {
+ result.put("message", "The source is valid");
+ }
return result;
}
+ /**
+ * What the tools say of a Citrus test: it is not a route, so only its
YAML syntax is checked, and there is no
+ * reload to wait for or to report (camel run --dev --source-dir loads no
routes from it); camel test run runs it.
+ */
+ static String citrusTestNote(String file) {
+ return "A Citrus test is not a route: only its YAML syntax is checked,
and no integration loads it. Run it with"
+ + " camel test run " + file + " in the project directory.";
+ }
+
/** The line of a validation message, "Line N: ...", or 0. */
private static int lineOf(String error) {
if (error == null || !error.startsWith("Line ")) {
@@ -723,9 +736,11 @@ public final class AuthoringTools {
}
if ("unchanged".equals(result.getString("status"))) {
result.put("message", "The edit leaves the file as it is (the
replacement gives the text the file already"
- + " has), so nothing was written and nothing
reloads. To change what the route does,"
- + " change the text; camel_get_log and
camel_get_errors show what the current version"
- + " does.");
+ + " has), so nothing was written and nothing
reloads. "
+ + (SourceValidator.isCitrusTest(file)
+ ? citrusTestNote(file)
+ : "To change what the route does,
change the text; camel_get_log and"
+ + " camel_get_errors show what the
current version does."));
} else if (!"invalid".equals(result.getString("status"))) {
result.put("status", "edited");
if (edit.get("matchedLines") instanceof JsonArray matched) {
@@ -1524,7 +1539,7 @@ public final class AuthoringTools {
if (!notes.isEmpty()) {
result.put("notes", new JsonArray(notes));
}
- } else if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+ } else if ((name.endsWith(".yaml") || name.endsWith(".yml")) &&
!SourceValidator.isCitrusTest(name)) {
// a JMS reply header set by hand, with nothing sent InOut; a to:
that copies the file a consumer moves
List<String> notes = new
ArrayList<>(HeaderChecks.replyHeaderNotes(content));
notes.addAll(WriteNotes.fileCopyNotes(content));
@@ -1680,6 +1695,8 @@ public final class AuthoringTools {
List<String> problemsBefore = List.of();
List<String> repaired = List.of();
List<String> repairable = List.of();
+ // a Citrus test is not a route: none of the repairs, notes and
reloads of a route apply to it
+ boolean citrusTest = SourceValidator.isCitrusTest(file);
if (validate && SourceValidator.isValidatableFile(file)) {
// a missing consumer of a direct: endpoint does not refuse the
write: it is often a file not written yet
List<String> errors = SourceValidator.validate(file, content,
ctx.catalog(), ctx.propertyLineValidator(), dir,
@@ -1694,7 +1711,7 @@ public final class AuthoringTools {
errors = newProblems(before, errors, stillThere);
problemsBefore = stillThere;
}
- if (!errors.isEmpty()) {
+ if (!errors.isEmpty() && !citrusTest) {
// CAMEL-25532: the shapes with one reading are repaired, and
the write goes ahead when that leaves no
// new problem; the answer says what was repaired
Repaired r = repair(ctx, dir, file, content, errors, before,
exists);
@@ -1744,8 +1761,10 @@ public final class AuthoringTools {
result.put("file", file);
result.put("directory", dir.toString());
result.put("message", "The file already has exactly this content,
so nothing was written and nothing"
- + " reloads. To change what the route does,
change the content; camel_get_log and"
- + " camel_get_errors show what the current
version does.");
+ + " reloads. " + (citrusTest
+ ? citrusTestNote(file)
+ : "To change what the route does,
change the content; camel_get_log and"
+ + " camel_get_errors show what the
current version does."));
return result;
}
// a client that asks before each write (camel ask, CAMEL-25676) shows
the change first; the others write as
@@ -1759,7 +1778,9 @@ public final class AuthoringTools {
String processName = null;
String sinceKey = null;
long watchPid = -1;
- if (SourceValidator.isValidatableFile(file) ||
reloadsWithRoutes(file)) {
+ // not for a Citrus test: dev mode with --source-dir reloads the
routes when it changes, but loads no route from
+ // it, and the answer said the integration reloaded a file that it
does not load (test-citrus-orders)
+ if (!citrusTest && (SourceValidator.isValidatableFile(file) ||
reloadsWithRoutes(file))) {
if (ctx.hasProcess()) {
watchPid = ctx.pid();
RuntimeHelper.ProcessInfo p =
RuntimeHelper.findProcess(Long.toString(ctx.pid()));
@@ -1809,7 +1830,7 @@ public final class AuthoringTools {
boolean replyByHand = false;
boolean copies = false;
List<String> unusedProperties = List.of();
- if (validate) {
+ if (validate && !citrusTest) {
putFileNotes(result, file, content);
String lower = file.toLowerCase(Locale.ROOT);
boolean yaml = lower.endsWith(".yaml") || lower.endsWith(".yml");
@@ -1861,6 +1882,8 @@ public final class AuthoringTools {
+ " content and write again (see
reload.message).";
default -> "Written; " + reload.getString("message");
});
+ } else if (citrusTest) {
+ result.put("message", citrusTestNote(file));
} else if (notWatchedBy >= 0) {
result.put("message", "The running integration (pid " +
notWatchedBy + ") was started with its own files"
+ " and does not reload when this file
changes; camel_control restart picks up the"
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 7e4bfc366be5..a9b14e74e2fb 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
@@ -17,10 +17,12 @@
package org.apache.camel.dsl.jbang.core.commands.ai;
import java.io.IOException;
+import java.io.StringReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -40,6 +42,13 @@ import org.apache.camel.dsl.yaml.validator.ProjectFacts;
import org.apache.camel.dsl.yaml.validator.YamlPointerLines;
import org.apache.camel.dsl.yaml.validator.YamlValidator;
import org.snakeyaml.engine.v2.nodes.Node;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.Mark;
+import org.yaml.snakeyaml.error.MarkedYAMLException;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.events.Event;
import static
org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.JAVA_CLASS_PATTERN;
import static
org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.JAVA_PACKAGE_PATTERN;
@@ -69,9 +78,20 @@ public final class SourceValidator {
|| name.endsWith(".java") || name.endsWith(".xsl") ||
name.endsWith(".xslt") || name.endsWith(".xml");
}
+ /** The names Citrus runs as a YAML test: name.citrus.yaml,
name.citrus.it.yaml, name.citrus-test.yaml... */
+ private static final Pattern CITRUS_TEST =
Pattern.compile(".+\\.citrus(?:[.-][^/\\\\]+)?\\.ya?ml");
+
+ /**
+ * Whether the file is a Citrus test, such as test/orders.citrus.it.yaml:
camel test run runs it, and no integration
+ * loads it. It is YAML of its own, not a Camel route, so only its YAML
syntax is checked.
+ */
+ public static boolean isCitrusTest(String fileName) {
+ return fileName != null &&
CITRUS_TEST.matcher(fileName.toLowerCase(Locale.ROOT)).matches();
+ }
+
/**
- * Validates source by file type: Camel YAML DSL for .yaml/.yml files,
Camel options for .properties files. Other
- * file types have no validation and yield no messages.
+ * Validates source by file type: Camel YAML DSL for .yaml/.yml files (a
Citrus test for its YAML syntax only),
+ * Camel options for .properties files. Other file types have no
validation and yield no messages.
*
* @param fileName the file name; its extension picks the checks
* @param content the source
@@ -117,6 +137,11 @@ public final class SourceValidator {
Path directory, YamlValidator schemaValidator, boolean
checkConsumers) {
Objects.requireNonNull(catalog, "catalog");
String name = fileName == null ? "" :
fileName.toLowerCase(Locale.ROOT);
+ if (isCitrusTest(name)) {
+ // a Citrus test is not a route: checked as one, a new test was
refused ("a Camel YAML file is a list of
+ // entries"), and a local model then wrote a route in its place
(test-citrus-orders of the benchmark)
+ return validateYamlSyntax(content);
+ }
if (name.endsWith(".yaml") || name.endsWith(".yml")) {
// a rest binding to an OpenAPI specification hides the verb, and
the specification is a file beside the
// route: read it, so that a GET operation is known to carry no
body (CAMEL-24844), and with the REST binding
@@ -617,6 +642,36 @@ public final class SourceValidator {
return XmlChecks.validateXml(content);
}
+ /**
+ * The YAML syntax error of a YAML file that is not a route, such as a
Citrus test, with its line: the parser stops
+ * at the first one. Empty when the content parses.
+ */
+ static List<String> validateYamlSyntax(String content) {
+ if (content == null || content.isBlank()) {
+ return List.of();
+ }
+ try {
+ Iterator<Event> events = new Yaml(new SafeConstructor(new
LoaderOptions())).parse(new StringReader(content))
+ .iterator();
+ while (events.hasNext()) {
+ events.next();
+ }
+ return List.of();
+ } catch (MarkedYAMLException e) {
+ Mark at = e.getProblemMark();
+ Mark from = e.getContextMark();
+ // where the parser was reading from, such as the line that opened
a quote that never closes
+ String context = e.getContext() != null && from != null
+ ? " (" + e.getContext() + " that starts at line " +
(from.getLine() + 1) + ")" : "";
+ return List.of(at != null
+ ? "Line " + (at.getLine() + 1) + ": the YAML does not
parse at column " + (at.getColumn() + 1) + ": "
+ + e.getProblem() + context
+ : "The YAML does not parse: " + e.getProblem() + context);
+ } catch (YAMLException e) {
+ return List.of("The YAML does not parse: " + e.getMessage());
+ }
+ }
+
/**
* A Camel* header that no component used in the file defines
(CamelTimerIndex; the timer sets CamelTimerCounter):
* the value is null at runtime. Checked against the header metadata of
every component the file names, with the
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
index ea135305d2e7..b6624e5d0dc1 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -1147,8 +1147,12 @@ public final class ToolRegistry {
= "Example name as listed, e.g. quick-start/timer-log or
fail-well/circuit-breaker (the short name without "
+ "the group, such as timer-log, works too)";
public static final String EXAMPLE_FILE_DESC
- = "File name within the example, as in its files, e.g.
timer-log.camel.yaml or application.properties "
- + "for quick-start/timer-log";
+ = "File name within the example, as in its files, e.g.
timer-log.camel.yaml, application.properties "
+ + "or test/timer-log.citrus.it.yaml (its Citrus test) for
quick-start/timer-log";
+ /** The filter of the example list, shared with the MCP server. */
+ public static final String EXAMPLE_FILTER_DESC
+ = "Filter by name, description or tag (case-insensitive; every
word must match); citrus finds the "
+ + "examples with a Citrus test";
private static void registerExampleTools() {
register(tool("list_examples",
@@ -1156,10 +1160,10 @@ public final class ToolRegistry {
+ "route, fail-well, connect,
connect-service, contracts, ai, cloud, showcase). "
+ "Returns the groups (level, title,
intro) and the examples in reading order with "
+ "name, title, description, level,
order, tags, what they teach, the infra "
- + "services they need, whether they are
bundled and their files. "
+ + "services they need, whether they are
bundled and their files (test/ has the "
+ + "Citrus test, run with camel test
run). "
+ "Call it without arguments for the
whole ladder, with level for one group.")
- .param("filter", "string",
- "Filter by name, description, or tag
(case-insensitive)", false)
+ .param("filter", "string", EXAMPLE_FILTER_DESC, false)
.param("level", "string", EXAMPLE_LEVEL_DESC, false)
.param("limit", "integer", "Maximum number of examples to
return (default: 50)", false)
.executor((ctx, args) -> {
@@ -1224,7 +1228,7 @@ public final class ToolRegistry {
}
jo.put("bundled", ExampleHelper.isBundled(entry));
jo.put("requiresDocker",
ExampleHelper.requiresDocker(entry));
- jo.put("files", ExampleHelper.getFiles(entry));
+ jo.put("files", exampleFiles(entry));
results.add(jo);
}
}
@@ -1259,7 +1263,7 @@ public final class ToolRegistry {
String fileName = file.trim();
JsonObject entry = resolveExample(ExampleHelper.loadCatalog(), wanted);
String name = entry.getString("name");
- List<String> files = ExampleHelper.getFiles(entry);
+ List<String> files = exampleFiles(entry);
if (!files.contains(fileName)) {
throw new ToolExecutionException(
"File '" + fileName + "' not found in example '" + name +
"'. Available files: " + files);
@@ -1284,6 +1288,17 @@ public final class ToolRegistry {
return answer;
}
+ /**
+ * The files of an example as the example tools list them: the files it
runs with, and its Citrus test, which the
+ * catalog leaves out. A model asked to write a Citrus test had no sample
to read (test-citrus-orders of the
+ * benchmark).
+ */
+ static List<String> exampleFiles(JsonObject entry) {
+ List<String> files = new ArrayList<>(ExampleHelper.getFiles(entry));
+ files.addAll(ExampleHelper.getTestFiles(entry));
+ return files;
+ }
+
/**
* The catalog entry of an example by its full name, or by its short name
when only one example has it, as camel run
* resolves it; an ambiguous short name answers with the candidates, an
unknown one with the closest names.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
index 97bdf84b2097..382c4b2a335e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java
@@ -141,47 +141,48 @@ public final class ExampleHelper {
return names;
}
+ /**
+ * The examples whose name, title, description, level or tags have every
word of the filter (case-insensitive), each
+ * word in any of them: "csv-to-json test" finds the example and its test.
An example with a Citrus test is found by
+ * citrus, test and its test file as well.
+ */
public static List<JsonObject> filterExamples(List<JsonObject> catalog,
String filter) {
- if (filter == null || filter.isEmpty()) {
+ if (filter == null || filter.isBlank()) {
return catalog;
}
- String lowerFilter = filter.toLowerCase();
+ List<String> words =
List.of(filter.toLowerCase(Locale.ROOT).trim().split("\\s+"));
List<JsonObject> result = new ArrayList<>();
for (JsonObject entry : catalog) {
- if (matches(entry, lowerFilter)) {
+ List<String> texts = searchTexts(entry);
+ if (words.stream().allMatch(w -> texts.stream().anyMatch(t ->
t.contains(w)))) {
result.add(entry);
}
}
return result;
}
+ /** What a filter searches in an example, in lower case. */
@SuppressWarnings("unchecked")
- private static boolean matches(JsonObject entry, String filter) {
- String name = entry.getString("name");
- if (name != null && name.toLowerCase().contains(filter)) {
- return true;
- }
- String title = entry.getString("title");
- if (title != null && title.toLowerCase().contains(filter)) {
- return true;
- }
- String desc = entry.getString("description");
- if (desc != null && desc.toLowerCase().contains(filter)) {
- return true;
- }
- String level = entry.getString("level");
- if (level != null && level.toLowerCase().contains(filter)) {
- return true;
+ private static List<String> searchTexts(JsonObject entry) {
+ List<String> texts = new ArrayList<>();
+ for (String key : new String[] { "name", "title", "description",
"level" }) {
+ String text = entry.getString(key);
+ if (text != null) {
+ texts.add(text.toLowerCase(Locale.ROOT));
+ }
}
Collection<String> tags = (Collection<String>) entry.get("tags");
if (tags != null) {
for (String tag : tags) {
- if (tag.toLowerCase().contains(filter)) {
- return true;
- }
+ texts.add(tag.toLowerCase(Locale.ROOT));
}
}
- return false;
+ if (hasCitrusTests(entry)) {
+ // the catalog has a flag for the test, not its file: a search for
citrus found no example
+ texts.add("citrus tests");
+ texts.addAll(getTestFiles(entry));
+ }
+ return texts;
}
/**
@@ -451,6 +452,19 @@ public final class ExampleHelper {
return citrus != null && citrus;
}
+ /**
+ * The Citrus test of an example, run with camel test run:
test/name.citrus.it.yaml, the name camel-jbang-examples
+ * gives it (its AGENTS.md). Empty for an example without one. The files
of the catalog are those the example runs
+ * with, so they leave out its test.
+ */
+ public static List<String> getTestFiles(JsonObject entry) {
+ String name = entry.getString("name");
+ if (!hasCitrusTests(entry) || name == null) {
+ return List.of();
+ }
+ return List.of("test/" + name.substring(name.lastIndexOf('/') + 1) +
".citrus.it.yaml");
+ }
+
/**
* What an example needs beyond its infra services, for the user to do
before the run (a model to pull, an API key
* to set, another example to run first), as plain text; null when it
needs nothing else.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/test/file-processing.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/test/file-processing.citrus.it.yaml
new file mode 100644
index 000000000000..00f2285e0a60
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/test/file-processing.citrus.it.yaml
@@ -0,0 +1,26 @@
+name: file-processing-test
+description: The file-processing example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "file-processing"
+ file: "../file-processing.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "file-processing"
+ logMessage: "Invoice INV-2001 for ORD-1001: 49.4 EUR"
+ - camel:
+ jbang:
+ verify:
+ integration: "file-processing"
+ logMessage: "Invoice invoice-2003.json rejected"
+ - camel:
+ jbang:
+ verify:
+ integration: "file-processing"
+ logMessage: "Invoice INV-2004 for ORD-1003: 53.45 EUR"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/http-client/test/http-client.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/http-client/test/http-client.citrus.it.yaml
new file mode 100644
index 000000000000..1d40c02c003a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/http-client/test/http-client.citrus.it.yaml
@@ -0,0 +1,21 @@
+name: http-client-test
+description: The http-client example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "http-client"
+ file: "../http-client.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "http-client"
+ logMessage: "ORD-1001: CAMEL-TSHIRT x 2, 120 in stock, ok"
+ - camel:
+ jbang:
+ verify:
+ integration: "http-client"
+ logMessage: "ORD-1003: CAMEL-CAP x 1, only 0 in stock, back-order"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/stock-api/test/stock-api.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/stock-api/test/stock-api.citrus.it.yaml
new file mode 100644
index 000000000000..7d653d229739
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/stock-api/test/stock-api.citrus.it.yaml
@@ -0,0 +1,49 @@
+name: stock-api-test
+description: The stock API answers one SKU as JSON and 404 for an unknown one
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "stock-api"
+ file: "../stock-api.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "stock.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "stock-api"
+ logMessage: "(stock-api) started"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/stock/CAMEL-MUG"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ contentType: "application/json"
+ validate:
+ - jsonPath:
+ - expression: "$.sku"
+ value: "CAMEL-MUG"
+ - expression: "$.qty"
+ value: "42"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/stock/CAMEL-SOCKS"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "404"
+ validate:
+ - jsonPath:
+ - expression: "$.error"
+ value: "unknown sku CAMEL-SOCKS"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-client/test/openapi-client.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-client/test/openapi-client.citrus.it.yaml
new file mode 100644
index 000000000000..69fdb646d8c0
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-client/test/openapi-client.citrus.it.yaml
@@ -0,0 +1,39 @@
+name: openapi-client-test
+description: The client reserves stock by contract against the openapi-server
example
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "openapi-server"
+ file: "../../openapi-server/openapi-server.camel.yaml"
+ systemProperties:
+ file: "../../openapi-server/application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "openapi-server"
+ logMessage: "(openapi-server) started"
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "openapi-client"
+ file: "../openapi-client.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "stock-api.json"
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "openapi-client"
+ logMessage: "ORD-1002: reserved 3 x CAMEL-MUG, 42 left on the shelf"
+ - camel:
+ jbang:
+ verify:
+ integration: "openapi-client"
+ logMessage: "ORD-1003: CAMEL-CAP not reserved, the stock API
answered 409"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/test/openapi-server.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/test/openapi-server.citrus.it.yaml
new file mode 100644
index 000000000000..6c3449edc783
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/openapi-server/test/openapi-server.citrus.it.yaml
@@ -0,0 +1,76 @@
+name: openapi-server-test
+description: The stock API answers by contract
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "openapi-server"
+ file: "../openapi-server.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "stock-api.json"
+ - "stock.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "openapi-server"
+ logMessage: "(openapi-server) started"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/api/stock/CAMEL-MUG"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ validate:
+ - jsonPath:
+ - expression: "$.qty"
+ value: "42"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ POST:
+ path: "/api/stock/CAMEL-MUG/reserve"
+ contentType: "application/json"
+ body:
+ data: '{"orderId": "ORD-1001", "qty": 2}'
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ validate:
+ - jsonPath:
+ - expression: "$.reserved"
+ value: "2"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ POST:
+ path: "/api/stock/CAMEL-CAP/reserve"
+ contentType: "application/json"
+ body:
+ data: '{"orderId": "ORD-1003", "qty": 1}'
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "409"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ POST:
+ path: "/api/stock/CAMEL-MUG/reserve"
+ contentType: "application/json"
+ body:
+ data: '{"orderId": "ORD-1001"}'
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "400"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/test/rest-code-first.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/test/rest-code-first.citrus.it.yaml
new file mode 100644
index 000000000000..b1d7cb212c97
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/contracts/rest-code-first/test/rest-code-first.citrus.it.yaml
@@ -0,0 +1,95 @@
+name: rest-code-first-test
+description: The order API answers as the README shows
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "rest-code-first"
+ file: "../rest-code-first.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "Order.java"
+ - "orders.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "rest-code-first"
+ logMessage: "(rest-code-first) started"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/orders/ORD-1002"
+ # the client's default Content-Type text/plain makes the json
binding skip writing the answer
+ contentType: "application/json"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ validate:
+ - jsonPath:
+ - expression: "$.customer"
+ value: "C-207"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/orders/ORD-9999"
+ # the client's default Content-Type text/plain makes the json
binding skip writing the answer
+ contentType: "application/json"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "404"
+ validate:
+ - jsonPath:
+ - expression: "$.error"
+ value: "unknown order ORD-9999"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ POST:
+ path: "/orders"
+ contentType: "application/json"
+ body:
+ data: '{"orderId": "ORD-1004", "customer": "C-482", "country":
"DK", "status": "paid", "lines": [{"sku": "CAMEL-MUG", "qty": 1, "price":
9.5}]}'
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "201"
+ validate:
+ - jsonPath:
+ - expression: "$.orderId"
+ value: "ORD-1004"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ POST:
+ path: "/orders"
+ contentType: "application/json"
+ body:
+ data: '{"orderId": "ORD-1005", "customer": "C-482", "country":
"DK", "status": "paid", "lines": []}'
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "400"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/openapi"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ validate:
+ - jsonPath:
+ - expression: "$.paths['/orders/{id}'].get.operationId"
+ value: "getOrder"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/test/circuit-breaker.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/test/circuit-breaker.citrus.it.yaml
new file mode 100644
index 000000000000..068b417252d5
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/circuit-breaker/test/circuit-breaker.citrus.it.yaml
@@ -0,0 +1,26 @@
+name: circuit-breaker-test
+description: The circuit-breaker example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "circuit-breaker"
+ file: "../circuit-breaker.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "circuit-breaker"
+ logMessage: "(breaker OPEN): supplier failed, using last known stock"
+ - camel:
+ jbang:
+ verify:
+ integration: "circuit-breaker"
+ logMessage: "(breaker OPEN): breaker open, supplier not called,
using last known stock"
+ - camel:
+ jbang:
+ verify:
+ integration: "circuit-breaker"
+ logMessage: "Stock check 18 (breaker CLOSED): CAMEL-MUG"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/test/error-handling.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/test/error-handling.citrus.it.yaml
new file mode 100644
index 000000000000..f6ab5fe607f1
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/test/error-handling.citrus.it.yaml
@@ -0,0 +1,36 @@
+name: error-handling-test
+description: The error-handling example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "error-handling"
+ file: "../error-handling.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "parked.camel.yaml"
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "error-handling"
+ logMessage: "Payment for ORD-1001 charged"
+ - camel:
+ jbang:
+ verify:
+ integration: "error-handling"
+ logMessage: "Payment for ORD-1002 charged"
+ - camel:
+ jbang:
+ verify:
+ integration: "error-handling"
+ logMessage: "Payment for ORD-1003 declined: card declined"
+ - camel:
+ jbang:
+ verify:
+ integration: "error-handling"
+ logMessage: "Order ORD-1003 parked for manual review"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/rest-api/test/rest-api.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/rest-api/test/rest-api.citrus.it.yaml
new file mode 100644
index 000000000000..160d16f68fba
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/rest-api/test/rest-api.citrus.it.yaml
@@ -0,0 +1,40 @@
+name: rest-api-test
+description: The REST API answers the greeting, with and without a name
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "rest-api"
+ file: "../rest-api.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "rest-api"
+ logMessage: "(rest-api) started"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/api/hello"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ body:
+ data: "Hello from Camel REST API!"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ GET:
+ path: "/api/hello/World"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "200"
+ body:
+ data: "Hello World from Camel REST API!"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/routes/test/routes.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/routes/test/routes.citrus.it.yaml
new file mode 100644
index 000000000000..b2f129e008d5
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/routes/test/routes.citrus.it.yaml
@@ -0,0 +1,19 @@
+name: routes-test
+description: The route logs the greeting the Greeter bean built
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "routes"
+ file: "../routes.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "beans.yaml"
+ - "Greeter.java"
+ - camel:
+ jbang:
+ verify:
+ integration: "routes"
+ logMessage: "Hello, Camel!"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/splitter/test/splitter.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/splitter/test/splitter.citrus.it.yaml
new file mode 100644
index 000000000000..d3ebde909d83
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/splitter/test/splitter.citrus.it.yaml
@@ -0,0 +1,19 @@
+name: splitter-test
+description: The splitter logs the batch and then one line per item
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "splitter"
+ file: "../splitter.camel.yaml"
+ - camel:
+ jbang:
+ verify:
+ integration: "splitter"
+ logMessage: "Received batch: Laptop,Phone,Tablet,Monitor,Keyboard"
+ - camel:
+ jbang:
+ verify:
+ integration: "splitter"
+ logMessage: "Processing item 4: Keyboard"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/timer-log/test/timer-log.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/timer-log/test/timer-log.citrus.it.yaml
new file mode 100644
index 000000000000..d2b2ebe41c0c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/quick-start/timer-log/test/timer-log.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: timer-log-test
+description: The timer route logs the greeting from application.properties
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "timer-log"
+ file: "../timer-log.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "timer-log"
+ logMessage: "Hello Camel!"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/aggregator/test/aggregator.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/aggregator/test/aggregator.citrus.it.yaml
new file mode 100644
index 000000000000..9d7e783f8ae2
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/aggregator/test/aggregator.citrus.it.yaml
@@ -0,0 +1,25 @@
+name: aggregator-test
+description: The aggregator example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "aggregator"
+ file: "../aggregator.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "aggregator"
+ logMessage: "Shipment for ORD-1001 complete"
+ - camel:
+ jbang:
+ verify:
+ integration: "aggregator"
+ logMessage: "Shipment for ORD-1003 complete"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/content-based-router/test/content-based-router.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/content-based-router/test/content-based-router.citrus.it.yaml
new file mode 100644
index 000000000000..508c9013789e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/content-based-router/test/content-based-router.citrus.it.yaml
@@ -0,0 +1,30 @@
+name: content-based-router-test
+description: The content-based-router example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "content-based-router"
+ file: "../content-based-router.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "content-based-router"
+ logMessage: "ORD-1001 from DK: local delivery"
+ - camel:
+ jbang:
+ verify:
+ integration: "content-based-router"
+ logMessage: "ORD-1002 from DE: EU shipping"
+ - camel:
+ jbang:
+ verify:
+ integration: "content-based-router"
+ logMessage: "ORD-1003 from US: export"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
new file mode 100644
index 000000000000..4794852fa00f
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
@@ -0,0 +1,35 @@
+name: dynamic-routing-test
+description: The dynamic-routing example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "dynamic-routing"
+ file: "../dynamic-routing.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "dynamic-routing"
+ logMessage: "Warehouse DK picks ORD-1001"
+ - camel:
+ jbang:
+ verify:
+ integration: "dynamic-routing"
+ logMessage: "Invoice for ORD-1001 via the backup service"
+ - camel:
+ jbang:
+ verify:
+ integration: "dynamic-routing"
+ logMessage: "Warehouse US picks ORD-1003"
+ - camel:
+ jbang:
+ verify:
+ integration: "dynamic-routing"
+ logMessage: "No invoice for ORD-1003 until it is paid"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/test/enrich-and-dedupe.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/test/enrich-and-dedupe.citrus.it.yaml
new file mode 100644
index 000000000000..b331d2b608df
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/enrich-and-dedupe/test/enrich-and-dedupe.citrus.it.yaml
@@ -0,0 +1,34 @@
+name: enrich-and-dedupe-test
+description: The enrich-and-dedupe example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "enrich-and-dedupe"
+ file: "../enrich-and-dedupe.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - "orders/resent-order-1001.json"
+ - "customers/C-482.json"
+ - "customers/C-207.json"
+ - "customers/C-134.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "enrich-and-dedupe"
+ logMessage: "Order ORD-1001 for Anna Jensen (gold)"
+ - camel:
+ jbang:
+ verify:
+ integration: "enrich-and-dedupe"
+ logMessage: "Order ORD-1001 is a duplicate, skipped"
+ - camel:
+ jbang:
+ verify:
+ integration: "enrich-and-dedupe"
+ logMessage: "Audited ORD-1003.json"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/filter-and-multicast/test/filter-and-multicast.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/filter-and-multicast/test/filter-and-multicast.citrus.it.yaml
new file mode 100644
index 000000000000..dae997f2b82c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/filter-and-multicast/test/filter-and-multicast.citrus.it.yaml
@@ -0,0 +1,30 @@
+name: filter-and-multicast-test
+description: The filter-and-multicast example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "filter-and-multicast"
+ file: "../filter-and-multicast.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "filter-and-multicast"
+ logMessage: "Warehouse: pick 2 line(s) for ORD-1001"
+ - camel:
+ jbang:
+ verify:
+ integration: "filter-and-multicast"
+ logMessage: "Invoicing: bill customer C-207 for ORD-1002"
+ - camel:
+ jbang:
+ verify:
+ integration: "filter-and-multicast"
+ logMessage: "Order ORD-1003 received, status pending"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/order-lines/test/order-lines.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/order-lines/test/order-lines.citrus.it.yaml
new file mode 100644
index 000000000000..6bbfb544fc89
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route/order-lines/test/order-lines.citrus.it.yaml
@@ -0,0 +1,25 @@
+name: order-lines-test
+description: The order-lines example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "order-lines"
+ file: "../order-lines.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "orders/order-1001.json"
+ - "orders/order-1002.json"
+ - "orders/order-1003.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "order-lines"
+ logMessage: "pick 2 x CAMEL-TSHIRT for ORD-1001"
+ - camel:
+ jbang:
+ verify:
+ integration: "order-lines"
+ logMessage: "Order ORD-1003: all 3 line(s) sent to picking"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/nightly-report/test/nightly-report.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/nightly-report/test/nightly-report.citrus.it.yaml
new file mode 100644
index 000000000000..f49d8660f902
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/nightly-report/test/nightly-report.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: nightly-report-test
+description: The scheduled report logs the stock counts
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "nightly-report"
+ file: "../nightly-report.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "nightly-report"
+ logMessage: "120 T-shirts and 45 mugs in stock"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/order-generator/test/order-generator.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/order-generator/test/order-generator.citrus.it.yaml
new file mode 100644
index 000000000000..5ffe7828be8a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/order-generator/test/order-generator.citrus.it.yaml
@@ -0,0 +1,24 @@
+name: order-generator-test
+description: The order generator logs a new order with a number and a JSON body
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "order-generator"
+ file: "../order-generator.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "beans.yaml"
+ - "OrderNumber.java"
+ - camel:
+ jbang:
+ verify:
+ integration: "order-generator"
+ logMessage: "New order ORD-1001"
+ - camel:
+ jbang:
+ verify:
+ integration: "order-generator"
+ logMessage: "\"sku\": \"CAMEL-TSHIRT\""
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/properties-and-profiles/test/properties-and-profiles.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/properties-and-profiles/test/properties-and-profiles.citrus.it.yaml
new file mode 100644
index 000000000000..774460494b22
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/run/properties-and-profiles/test/properties-and-profiles.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: properties-and-profiles-test
+description: The route greets with the values of application.properties
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "properties-and-profiles"
+ file: "../properties-and-profiles.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "properties-and-profiles"
+ logMessage: "Welcome to Camel Shop (development), prices in EUR"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/test/csv-to-json.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/test/csv-to-json.citrus.it.yaml
new file mode 100644
index 000000000000..87a05fcb758f
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/test/csv-to-json.citrus.it.yaml
@@ -0,0 +1,21 @@
+name: csv-to-json-test
+description: The csv-to-json example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "csv-to-json"
+ file: "../csv-to-json.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "csv-to-json"
+ logMessage: "Invoice INV-2001"
+ - camel:
+ jbang:
+ verify:
+ integration: "csv-to-json"
+ logMessage: "Invoice INV-2003"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/data-mapping/test/data-mapping.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/data-mapping/test/data-mapping.citrus.it.yaml
new file mode 100644
index 000000000000..7a9eb375cef4
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/data-mapping/test/data-mapping.citrus.it.yaml
@@ -0,0 +1,24 @@
+name: data-mapping-test
+description: The data-mapping example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "data-mapping"
+ file: "../data-mapping.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "order.json"
+ - "shipment-mapping.groovy"
+ - camel:
+ jbang:
+ verify:
+ integration: "data-mapping"
+ logMessage: "\"shipmentRef\":\"SHIP-1001\""
+ - camel:
+ jbang:
+ verify:
+ integration: "data-mapping"
+ logMessage: "\"totalPieces\":3"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/groovy/test/groovy.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/groovy/test/groovy.citrus.it.yaml
new file mode 100644
index 000000000000..f4148cb2419c
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/groovy/test/groovy.citrus.it.yaml
@@ -0,0 +1,24 @@
+name: groovy-test
+description: The groovy example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "groovy"
+ file: "../groovy.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "order.json"
+ - "order-bad-email.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "groovy"
+ logMessage: "Order ORD-1001 accepted"
+ - camel:
+ jbang:
+ verify:
+ integration: "groovy"
+ logMessage: "Order ORD-1002 rejected"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/json-transform/test/json-transform.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/json-transform/test/json-transform.citrus.it.yaml
new file mode 100644
index 000000000000..906e20673d31
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/json-transform/test/json-transform.citrus.it.yaml
@@ -0,0 +1,23 @@
+name: json-transform-test
+description: The json-transform example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "json-transform"
+ file: "../json-transform.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "order.json"
+ - camel:
+ jbang:
+ verify:
+ integration: "json-transform"
+ logMessage: "\"order\" : \"ORD-1001\""
+ - camel:
+ jbang:
+ verify:
+ integration: "json-transform"
+ logMessage: "\"sku\" : \"CAMEL-TSHIRT\""
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xml-to-json/test/xml-to-json.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xml-to-json/test/xml-to-json.citrus.it.yaml
new file mode 100644
index 000000000000..f46df6a5532b
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xml-to-json/test/xml-to-json.citrus.it.yaml
@@ -0,0 +1,16 @@
+name: xml-to-json-test
+description: The xml-to-json example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "xml-to-json"
+ file: "../xml-to-json.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - camel:
+ jbang:
+ verify:
+ integration: "xml-to-json"
+ logMessage: "The same order as JSON: {\"id\":\"ORD-1001\""
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xslt/test/xslt.citrus.it.yaml
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xslt/test/xslt.citrus.it.yaml
new file mode 100644
index 000000000000..b1078a001031
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/xslt/test/xslt.citrus.it.yaml
@@ -0,0 +1,23 @@
+name: xslt-test
+description: The xslt example logs what its README promises
+actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "xslt"
+ file: "../xslt.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ resources:
+ - "packing-slip.xsl"
+ - camel:
+ jbang:
+ verify:
+ integration: "xslt"
+ logMessage: "<packingSlip order=\"ORD-1001\""
+ - camel:
+ jbang:
+ verify:
+ integration: "xslt"
+ logMessage: "<pieces>3</pieces>"
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsCitrusTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsCitrusTest.java
new file mode 100644
index 000000000000..2492bc9f7db9
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsCitrusTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.ai;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A Citrus test, such as test/orders.citrus.it.yaml, is run with camel test
run and is not a route: the write, edit and
+ * validate tools check its YAML syntax only, with none of the repairs, notes
and reloads of a route (test-citrus-orders
+ * of the local-model benchmark).
+ */
+class AuthoringToolsCitrusTest {
+
+ private static final String FILE = "test/orders.citrus.it.yaml";
+
+ /** The sample test camel test init writes. */
+ private static final String SAMPLE = """
+ name: orders.citrus.it
+ author: Citrus
+ status: FINAL
+ description: Sample test in YAML
+ variables:
+ - name: message
+ value: Citrus rocks!
+ actions:
+ - echo:
+ message: "${message}"
+ """;
+
+ /** The test step 1 of test-citrus-orders asks for. */
+ private static final String ORDERS_TEST = """
+ name: orders-test
+ description: The order API accepts the sample order and logs it
+ actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ name: "orders"
+ file: "../orders.camel.yaml"
+ systemProperties:
+ file: "../application.properties"
+ - http:
+ client: "http://localhost:8080"
+ sendRequest:
+ POST:
+ path: "/orders"
+ contentType: "application/json"
+ body:
+ resource:
+ file: "../sample-order.json"
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "201"
+ - camel:
+ jbang:
+ verify:
+ integration: "orders"
+ logMessage: "Order ORD-1001 accepted for C-482 with 2
line(s)"
+ """;
+
+ private static final String LOG_MESSAGE = " logMessage: \"Order
ORD-1001 accepted for C-482 with 2 line(s)\"\n";
+
+ @Test
+ void theNamesCitrusRunsAsYamlTests() {
+ assertThat(SourceValidator.isCitrusTest(FILE)).isTrue();
+
assertThat(SourceValidator.isCitrusTest("orders.citrus.yaml")).isTrue();
+
assertThat(SourceValidator.isCitrusTest("orders.citrus.test.yaml")).isTrue();
+
assertThat(SourceValidator.isCitrusTest("orders.citrus-it.yaml")).isTrue();
+
assertThat(SourceValidator.isCitrusTest("test/Orders.Citrus.IT.yml")).isTrue();
+
assertThat(SourceValidator.isCitrusTest("orders.camel.yaml")).isFalse();
+ assertThat(SourceValidator.isCitrusTest("citrus.yaml")).isFalse();
+
assertThat(SourceValidator.isCitrusTest("orders.citrus.it.xml")).isFalse();
+
assertThat(SourceValidator.isCitrusTest("test.citrus.d/orders.yaml")).isFalse();
+
assertThat(SourceValidator.isCitrusTest("test/citrus-application.properties")).isFalse();
+ }
+
+ @Test
+ void aCitrusTestIsWrittenWithoutTheChecksOfARoute(@TempDir Path dir)
throws IOException {
+ // step 1: written over the sample of camel test init, the test was
said to have the problem "a Camel YAML file
+ // is a list of entries", and the model made it a list, then a route
+ Files.createDirectories(dir.resolve("test"));
+ Files.writeString(dir.resolve(FILE), SAMPLE);
+
+ JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir,
FILE, ORDERS_TEST, true);
+
+
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("overwritten");
+ assertThat(result.get("existingProblems")).isNull();
+ assertThat(result.get("notes")).isNull();
+
assertThat(result.getString("message")).isEqualTo(AuthoringTools.citrusTestNote(FILE))
+ .contains("camel test run test/orders.citrus.it.yaml");
+ assertThat(Files.readString(dir.resolve(FILE))).isEqualTo(ORDERS_TEST);
+ // the content a route check refuses
+ assertThat(SourceValidator.validateCamelYaml(ORDERS_TEST, new
DefaultCamelCatalog())).isNotEmpty();
+ }
+
+ @Test
+ void theVariablesOfACitrusTestAreNotSimpleExpressions(@TempDir Path dir) {
+ // the first write of the model was refused: Simple syntax error:
Unknown function: orderId
+ JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir,
FILE, """
+ name: orders-test
+ variables:
+ - name: orderId
+ value: ORD-1001
+ actions:
+ - echo:
+ message: "Order ${orderId} accepted"
+ """, true);
+
+
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("created");
+ }
+
+ @Test
+ void
aCitrusTestThatDoesNotParseIsRefusedWithTheLineAndNotRepaired(@TempDir Path
dir) throws IOException {
+ // a list item two columns off: the write of a route repairs it, the
write of a test says where it is
+ Files.createDirectories(dir.resolve("test"));
+ Files.writeString(dir.resolve(FILE), SAMPLE);
+
+ JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir,
FILE, """
+ name: orders-test
+ actions:
+ - camel:
+ jbang:
+ run:
+ integration:
+ file: "../orders.camel.yaml"
+ - http:
+ client: "http://localhost:8080"
+ """, true);
+
+
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("invalid");
+ assertThat(result.get("errors").toString()).contains("Line 8: the YAML
does not parse at column 5")
+ .contains("while parsing a block mapping that starts at line
3");
+ assertThat(result.get("repaired")).isNull();
+ assertThat(result.get("repairable")).isNull();
+ assertThat(Files.readString(dir.resolve(FILE))).isEqualTo(SAMPLE);
+ }
+
+ @Test
+ void theRunningIntegrationIsNotSaidToReloadACitrusTest(@TempDir Path dir) {
+ // the answer said "The running integration reloaded the file": dev
mode with --source-dir reloads the routes
+ // when the test changes, but loads no route from it
+ ToolContext ctx = new ToolContext();
+ ctx.selectProcess(ProcessHandle.current().pid());
+
+ JsonObject result = AuthoringTools.writeFile(ctx, dir, FILE,
ORDERS_TEST, true);
+
+ assertThat(result.getString("status")).isEqualTo("created");
+ assertThat(result.get("reload")).as(String.valueOf(result)).isNull();
+
assertThat(result.getString("message")).doesNotContain("reload").contains("camel
test run");
+ }
+
+ @Test
+ void anEditOfACitrusTestIsCheckedForItsYamlOnly(@TempDir Path dir) throws
IOException {
+ // step 2: a case added after the last action
+ Files.createDirectories(dir.resolve("test"));
+ Files.writeString(dir.resolve(FILE), ORDERS_TEST);
+
+ JsonObject result = AuthoringTools.editFile(new ToolContext(), dir,
FILE, LOG_MESSAGE, LOG_MESSAGE + """
+ - http:
+ client: "http://localhost:8080"
+ receiveResponse:
+ response:
+ status: "400"
+ """);
+
+
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("edited");
+ assertThat(result.get("reload")).isNull();
+ assertThat(result.getString("message")).contains("camel test run " +
FILE);
+ assertThat(Files.readString(dir.resolve(FILE))).endsWith("
status: \"400\"\n");
+
+ JsonObject again = AuthoringTools.editFile(new ToolContext(), dir,
FILE, LOG_MESSAGE, LOG_MESSAGE);
+ assertThat(again.getString("status")).isEqualTo("unchanged");
+ assertThat(again.getString("message")).contains("camel test
run").doesNotContain("route does");
+ }
+
+ @Test
+ void validateSourceChecksTheYamlOfACitrusTest() {
+ JsonObject valid = AuthoringTools.validate(new ToolContext(), FILE,
ORDERS_TEST);
+
assertThat(valid.getBoolean("valid")).as(String.valueOf(valid)).isTrue();
+ assertThat(valid.getString("message")).startsWith("The YAML is
valid.").contains("camel test run " + FILE);
+
+ // a quote that is not closed: the parser reads to the end of the
file, and names the line of the quote
+ JsonObject invalid = AuthoringTools.validate(new ToolContext(), FILE,
"""
+ name: orders-test
+ actions:
+ - echo:
+ message: "Order accepted
+ - echo:
+ message: done
+ """);
+ assertThat(invalid.getBoolean("valid")).isFalse();
+ assertThat(invalid.get("errors").toString()).contains("the YAML does
not parse")
+ .contains("while scanning a quoted scalar that starts at line
4");
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
index 843caf7bab0b..e73ebf10e32b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExampleToolsRegistryTest.java
@@ -64,6 +64,51 @@ class ExampleToolsRegistryTest {
assertThat(bundled).as("bundled
examples").contains("quick-start/timer-log").hasSizeGreaterThan(10);
}
+ @Test
+ void theCitrusTestOfAnExampleIsOneOfItsFiles() throws Exception {
+ // test-citrus-orders of the benchmark: asked to write a Citrus test,
a model found no sample to read, as the
+ // files of the catalog leave out test/
+ int tests = 0;
+ for (JsonObject entry : rawCatalog()) {
+ if (!entry.getBooleanOrDefault("hasCitrusTests", false)) {
+ continue;
+ }
+ tests++;
+ String name = entry.getString("name");
+ String test = "test/" + name.substring(name.lastIndexOf('/') + 1)
+ ".citrus.it.yaml";
+
+ JsonObject answer = exampleFile(name, test);
+
+ if (entry.getBooleanOrDefault("bundled", false)) {
+ assertThat(answer.getString("content")).as("test of %s",
name).contains("actions:")
+ .isEqualTo(resource("examples/" + name + "/" + test));
+ } else {
+ assertThat(answer.getString("githubUrl")).as("test of %s",
name).endsWith("/" + name + "/" + test);
+ }
+ }
+ assertThat(tests).isGreaterThan(20);
+ }
+
+ @Test
+ void aSearchForCitrusListsTheExamplesWithATest() throws Exception {
+ long withTest = rawCatalog().stream().filter(e ->
e.getBooleanOrDefault("hasCitrusTests", false)).count();
+
+ JsonObject citrus = listExamples("citrus");
+
+ assertThat(citrus.getInteger("total")).isEqualTo((int) withTest);
+ for (Object o : citrus.getCollection("examples")) {
+ JsonObject example = (JsonObject) o;
+ String name = example.getString("name");
+ assertThat(example.<List<String>>
getCollection("files")).as("files of %s", name)
+ .contains("test/" + name.substring(name.lastIndexOf('/') +
1) + ".citrus.it.yaml");
+ }
+ // the test of one example
+ JsonObject one = listExamples("csv-to-json test");
+ assertThat(one.getInteger("total")).isEqualTo(1);
+ assertThat(((JsonObject)
one.getCollection("examples").iterator().next()).getString("name"))
+ .isEqualTo("transform/csv-to-json");
+ }
+
@Test
void anUnknownExampleListsTheClosestNames() {
// the group of 4.22 (beginner), a typo, and a name that is nowhere
near an example
@@ -118,10 +163,12 @@ class ExampleToolsRegistryTest {
ToolDescriptor tool = ToolRegistry.findTool("get_example_file");
assertThat(tool).isNotNull();
assertThat(ToolRegistry.EXAMPLE_NAME_DESC).contains("quick-start/timer-log");
-
assertThat(ToolRegistry.EXAMPLE_FILE_DESC).contains("timer-log.camel.yaml");
+
assertThat(ToolRegistry.EXAMPLE_FILE_DESC).contains("timer-log.camel.yaml",
"test/timer-log.citrus.it.yaml");
JsonObject answer = exampleFile("quick-start/timer-log",
"timer-log.camel.yaml");
assertThat(answer.getString("example")).isEqualTo("quick-start/timer-log");
assertThat(answer.getString("content")).contains("uri: timer");
+ assertThat(exampleFile("quick-start/timer-log",
"test/timer-log.citrus.it.yaml").getString("content"))
+ .contains("actions:");
}
private static JsonObject exampleFile(String example, String file) {
@@ -134,6 +181,11 @@ class ExampleToolsRegistryTest {
}
}
+ private static JsonObject listExamples(String filter) throws Exception {
+ Object result = ToolRegistry.execute("list_examples", new
ToolContext(), Map.of("filter", filter));
+ return (JsonObject) Jsoner.deserialize(result.toString());
+ }
+
private static List<JsonObject> rawCatalog() throws Exception {
JsonArray array = (JsonArray)
Jsoner.deserialize(resource("examples/camel-jbang-example-catalog.json"));
List<JsonObject> catalog = new ArrayList<>();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
index de60251429cc..b423a88ff8f3 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java
@@ -100,6 +100,35 @@ class ExampleHelperTest {
assertEquals("connect-service/mqtt",
filtered.get(0).getString("name"));
}
+ @Test
+ void shouldFilterByCitrusAndByEveryWord() {
+ List<JsonObject> catalog = ExampleHelper.loadCatalog();
+ // the catalog has a flag for the Citrus test of an example, not its
file: a search for citrus found none
+ List<JsonObject> citrus = ExampleHelper.filterExamples(catalog,
"citrus");
+
assertEquals(catalog.stream().filter(ExampleHelper::hasCitrusTests).count(),
citrus.size());
+ assertEquals(citrus.size(), ExampleHelper.filterExamples(catalog,
"Citrus tests").size());
+
+ // every word is found, each in any field
+ List<JsonObject> csv = ExampleHelper.filterExamples(catalog,
"csv-to-json test");
+ assertEquals(1, csv.size());
+ assertEquals("transform/csv-to-json", csv.get(0).getString("name"));
+ assertTrue(ExampleHelper.filterExamples(catalog, "memory-leak
test").isEmpty(), "the example has no test");
+ assertEquals(catalog.size(), ExampleHelper.filterExamples(catalog, "
").size());
+ }
+
+ @Test
+ void shouldGetTestFiles() {
+ List<JsonObject> catalog = ExampleHelper.loadCatalog();
+ JsonObject csv = ExampleHelper.findExample(catalog,
"transform/csv-to-json");
+ assertEquals(List.of("test/csv-to-json.citrus.it.yaml"),
ExampleHelper.getTestFiles(csv));
+ // not a file to run: camel run --example runs the files of the catalog
+ assertFalse(ExampleHelper.getFiles(csv).stream().anyMatch(f ->
f.startsWith("test/")));
+
+ JsonObject leak = ExampleHelper.findExample(catalog,
"showcase/memory-leak");
+ assertFalse(ExampleHelper.hasCitrusTests(leak));
+ assertTrue(ExampleHelper.getTestFiles(leak).isEmpty());
+ }
+
@Test
void shouldReturnAllWhenFilterEmpty() {
List<JsonObject> catalog = ExampleHelper.loadCatalog();
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
index 003c76299bb8..da53c3ad410c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
@@ -23,6 +23,7 @@ import java.nio.file.Path;
import java.util.Properties;
import java.util.stream.Stream;
+import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
import org.apache.camel.dsl.yaml.validator.YamlValidator;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.model.Model;
@@ -53,6 +54,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* Every example is also validated against the canonical YAML schema: the
examples are what a user or an AI agent starts
* from, so they must be written in the canonical format and not in the
deprecated compact notation, which
* {@code camel run} warns about.
+ *
+ * The Citrus tests of the examples are not routes, so they are left out.
*/
class ExampleRoutesLoadTest {
@@ -75,7 +78,8 @@ class ExampleRoutesLoadTest {
return files.filter(Files::isRegularFile)
.filter(p -> {
String name = p.getFileName().toString();
- return name.endsWith(".yaml") || name.endsWith(".yml");
+ // a Citrus test (test/name.citrus.it.yaml) is run by
camel test run, it is not a route
+ return (name.endsWith(".yaml") ||
name.endsWith(".yml")) && !SourceValidator.isCitrusTest(name);
})
.sorted()
// use the path relative to the examples root as a stable,
readable test display name
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
index f39e3fad1e99..3b5ecfcee05c 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
@@ -43,13 +43,14 @@ public class ExampleTools {
+ "cloud and showcase, in that reading order. Returns
the groups (level, title, introduction) "
+ "and the examples in reading order with name, title,
description, level, order, tags, "
+ "what they teach (components, EIPs, languages, data
formats), the infra services they need "
- + "(start them with camel infra run), whether they are
bundled and their files. "
+ + "(start them with camel infra run), whether they are
bundled and their files "
+ + "(test/ has the Citrus test, run with camel test
run). "
+ "Call it without arguments for the whole ladder,
with level for one group, "
- + "or with filter to search by name, description or
tag. "
+ + "or with filter to search by name, description or
tag (citrus for the examples with a "
+ + "Citrus test). "
+ "Use camel_catalog_example_file to read a file of an
example.")
public JsonObject camel_catalog_examples(
- @ToolArg(description = "Filter examples by name, description, or
tag (case-insensitive substring match)",
- required = false) String filter,
+ @ToolArg(description = ToolRegistry.EXAMPLE_FILTER_DESC, required
= false) String filter,
@ToolArg(description = ToolRegistry.EXAMPLE_LEVEL_DESC, required =
false) String level,
@ToolArg(description = "Maximum number of examples to return
(default: 50)",
required = false) Integer limit) {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
index f6f8feeae963..39d642aaace4 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructions.java
@@ -77,6 +77,8 @@ public class McpServerInstructions implements
InitialResponseInfo {
- which component for a protocol or product: camel_catalog_find
- a validated YAML sample and where it goes: camel_catalog_sample
- complete example projects: camel_catalog_examples
+ - a sample Citrus test (test/<name>.citrus.it.yaml, run with camel
test run): the test/ file of an \
+ example, camel_catalog_examples with filter citrus
- Maven coordinates (the --dep value) of a Java class:
camel_dependency_for_class
- validate YAML/Java/XML/properties before writing:
camel_validate_source
- run an integration: camel_run; its log: camel_get_log; its
errors: camel_get_errors
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
index dc3ab34208c7..d4e6529bbaca 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleToolsTest.java
@@ -93,6 +93,22 @@ class ExampleToolsTest {
assertThat(full.getString("content")).isEqualTo(answer.getString("content"));
}
+ @Test
+ void aSearchForCitrusFindsTheExamplesWithATestToRead() {
+ // test-citrus-orders of the benchmark: a search for citrus found no
example, and the files of an example left
+ // out its Citrus test
+ JsonObject citrus = new
ExampleTools().camel_catalog_examples("citrus", null, null);
+
+ List<JsonObject> examples = objects(citrus, "examples");
+ assertThat(examples).hasSizeGreaterThan(20);
+ assertThat(examples).allSatisfy(e -> assertThat(e.<List<String>>
getCollection("files"))
+ .anyMatch(f -> f.startsWith("test/") &&
f.endsWith(".citrus.it.yaml")));
+
+ JsonObject test = new
ExampleTools().camel_catalog_example_file("csv-to-json",
"test/csv-to-json.citrus.it.yaml");
+
assertThat(test.getString("example")).isEqualTo("transform/csv-to-json");
+ assertThat(test.getString("content")).contains("actions:",
"../csv-to-json.camel.yaml");
+ }
+
@Test
void errorsAreToolErrors() {
// the messages are tested on the registry tools; here only that they
reach the client as tool errors
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
index 1ebd193fca08..a458e1e269f8 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpServerInstructionsTest.java
@@ -88,6 +88,13 @@ class McpServerInstructionsTest {
assertThat(McpServerInstructions.MAPPED_TOOLS).containsExactlyInAnyOrderElementsOf(named);
}
+ @Test
+ void aSampleCitrusTestIsInTheExamples() {
+ String text = new McpServerInstructions().text();
+
+ assertThat(text).contains("Citrus test", "camel test run",
"camel_catalog_examples with filter citrus");
+ }
+
@Test
void anExplicitPropertyReplacesTheText() {
McpServerInstructions bean = new McpServerInstructions("4.0.0");