From 8252a40485fb8cebbe4380baf0b8d5d8f8cc274d Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 18 Oct 2026 08:33:33 +0200 Subject: [PATCH 1/3] CAMEL-25744: camel-jbang - the AI validator reports a comparison of CamelTimerCounter that can never be true, as the counter starts at 1 In ten benchmark runs a local model compared the counter with 0 eight times to pick the first fire. The counter counts the fires from 1, so == 0 is never true and both fires took the second file. The validator now reports a comparison of CamelTimerCounter that can never be true (== 0, <= 0, < 1) or never be false (!= 0), in Simple and in Groovy, with a hint to compare with 1. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../jbang/core/commands/ai/HeaderChecks.java | 34 +++++++++++++++++ .../ai/SourceValidatorEndpointTest.java | 38 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java index 0960cdb14c739..8137468b76aca 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java @@ -57,6 +57,15 @@ private HeaderChecks() { */ static final Map METADATA_OPTION = Map.of("timer", "includeMetadata"); + /** + * A comparison of the timer's counter with a number: the counter counts the fires from 1, so {@code == 0} is never + * true (CAMEL-25744: in 8 of 10 benchmark runs a model compared it with 0 to pick the first fire). The Simple forms + * (${exchangeProperty.CamelTimerCounter} == 0, ${header.CamelTimerCounter == 0 ? ...}) and the Groovy ones + * (exchange.getProperty('CamelTimerCounter', Integer) == 0). + */ + static final Pattern TIMER_COUNTER_COMPARISON = Pattern.compile( + "CamelTimerCounter(?:\\}|['\"]\\s*(?:,\\s*[\\w.]+\\s*)?\\))?\\s*(==|!=|<=|<)\\s*['\"]?(\\d+)['\"]?"); + static final Pattern EXCHANGE_PROPERTY_REF_PATTERN = Pattern.compile("\\$\\{exchangeProperty\\.(Camel[A-Z][A-Za-z0-9]*)"); /** Whether the option that makes the component set its exchange properties is true somewhere in the content. */ @@ -106,11 +115,36 @@ static String intentHint(String name) { return null; } + /** + * The comparisons of CamelTimerCounter that can never be true (== 0, <= 0, < 1) or never be false (!= 0), as the + * counter is 1 on the first fire. + */ + static List timerCounterComparisons(String content) { + List msgs = new ArrayList<>(); + String[] lines = content.split("\n", -1); + for (int i = 0; i < lines.length; i++) { + Matcher m = TIMER_COUNTER_COMPARISON.matcher(lines[i]); + while (m.find()) { + String op = m.group(1); + int n = Integer.parseInt(m.group(2)); + boolean never = ("==".equals(op) || "<=".equals(op)) && n == 0 || "<".equals(op) && n <= 1; + boolean always = "!=".equals(op) && n == 0; + if (never || always) { + msgs.add("Line " + (i + 1) + ": CamelTimerCounter counts the fires from 1 (the first fire is 1), so " + + op + " " + n + " is " + (never ? "never true" : "always true") + + ": compare with 1 to pick the first fire"); + } + } + } + return msgs; + } + public static List validateKnownHeaders(String content, CamelCatalog catalog) { List msgs = new ArrayList<>(); if (content == null) { return msgs; } + msgs.addAll(timerCounterComparisons(content)); Set known = new LinkedHashSet<>(); Map owner = new LinkedHashMap<>(); Matcher sm = SCHEME_IN_URI_PATTERN.matcher(content); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java index afaa47ac342c1..99da10df5b829 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java @@ -457,6 +457,44 @@ void aTimerExchangePropertyIsSetOnlyWithIncludeMetadata() { assertThat(msgs.get(1)).startsWith("Line 4:").contains("timer sets CamelTimerName only with includeMetadata=true"); } + /** CAMEL-25744: the counter is 1 on the first fire, so a comparison with 0 never picks it. */ + @Test + void aTimerCounterComparisonThatCanNeverBeTrueIsReported() { + List msgs = SourceValidator.validateKnownHeaders(""" + - from: + uri: "timer:tick?repeatCount=2&includeMetadata=true" + steps: + - setBody: + simple: "resource:file:${exchangeProperty.CamelTimerCounter == 0 ? 'a.json' : 'b.json'}" + - choice: + when: + - simple: "${header.CamelTimerCounter} != 0" + steps: + - log: "${body}" + - filter: + groovy: "exchange.getProperty('CamelTimerCounter', Integer) < 1" + """, catalog); + assertThat(msgs).hasSize(3); + assertThat(msgs.get(0)).startsWith("Line 5:").contains("counts the fires from 1").contains("== 0 is never true") + .contains("compare with 1"); + assertThat(msgs.get(1)).startsWith("Line 8:").contains("!= 0 is always true"); + assertThat(msgs.get(2)).startsWith("Line 12:").contains("< 1 is never true"); + } + + @Test + void aTimerCounterComparedWithOneIsFine() { + List msgs = SourceValidator.validateKnownHeaders(""" + - from: + uri: "timer:tick?repeatCount=2&includeMetadata=true" + steps: + - setBody: + simple: "resource:file:${exchangeProperty.CamelTimerCounter == 1 ? 'a.json' : 'b.json'}" + - filter: + simple: "${exchangeProperty.CamelTimerCounter} > 1" + """, catalog); + assertThat(msgs).isEmpty(); + } + @Test void withIncludeMetadataAHeaderLookupFindsTheExchangeProperty() { // Simple's ${header.x} falls back to the exchange properties, so it reads the counter From 0428ceaf55d7aa0c263955b8f9ce53c891f9d993 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 18 Oct 2026 09:18:27 +0200 Subject: [PATCH 2/3] CAMEL-25744: camel-jbang - also report > 0, >= 0 and >= 1 on CamelTimerCounter, which are always true Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../jbang/core/commands/ai/HeaderChecks.java | 8 ++++---- .../ai/SourceValidatorEndpointTest.java | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java index 8137468b76aca..6fc97eedcc480 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java @@ -64,7 +64,7 @@ private HeaderChecks() { * (exchange.getProperty('CamelTimerCounter', Integer) == 0). */ static final Pattern TIMER_COUNTER_COMPARISON = Pattern.compile( - "CamelTimerCounter(?:\\}|['\"]\\s*(?:,\\s*[\\w.]+\\s*)?\\))?\\s*(==|!=|<=|<)\\s*['\"]?(\\d+)['\"]?"); + "CamelTimerCounter(?:\\}|['\"]\\s*(?:,\\s*[\\w.]+\\s*)?\\))?\\s*(==|!=|<=|<|>=|>)\\s*['\"]?(\\d+)['\"]?"); static final Pattern EXCHANGE_PROPERTY_REF_PATTERN = Pattern.compile("\\$\\{exchangeProperty\\.(Camel[A-Z][A-Za-z0-9]*)"); @@ -116,8 +116,8 @@ static String intentHint(String name) { } /** - * The comparisons of CamelTimerCounter that can never be true (== 0, <= 0, < 1) or never be false (!= 0), as the - * counter is 1 on the first fire. + * The comparisons of CamelTimerCounter that can never be true (== 0, <= 0, < 1) or never be false (!= 0, > 0, >= + * 1), as the counter is 1 on the first fire. */ static List timerCounterComparisons(String content) { List msgs = new ArrayList<>(); @@ -128,7 +128,7 @@ static List timerCounterComparisons(String content) { String op = m.group(1); int n = Integer.parseInt(m.group(2)); boolean never = ("==".equals(op) || "<=".equals(op)) && n == 0 || "<".equals(op) && n <= 1; - boolean always = "!=".equals(op) && n == 0; + boolean always = ("!=".equals(op) || ">".equals(op)) && n == 0 || ">=".equals(op) && n <= 1; if (never || always) { msgs.add("Line " + (i + 1) + ": CamelTimerCounter counts the fires from 1 (the first fire is 1), so " + op + " " + n + " is " + (never ? "never true" : "always true") diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java index 99da10df5b829..4fde550d4cf79 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java @@ -481,6 +481,22 @@ void aTimerCounterComparisonThatCanNeverBeTrueIsReported() { assertThat(msgs.get(2)).startsWith("Line 12:").contains("< 1 is never true"); } + @Test + void aTimerCounterComparisonThatIsAlwaysTrueIsReported() { + List msgs = SourceValidator.validateKnownHeaders(""" + - from: + uri: "timer:tick?repeatCount=2&includeMetadata=true" + steps: + - filter: + simple: "${exchangeProperty.CamelTimerCounter} > 0" + - filter: + groovy: "exchange.getProperty('CamelTimerCounter', Integer) >= 1" + """, catalog); + assertThat(msgs).hasSize(2); + assertThat(msgs.get(0)).startsWith("Line 5:").contains("> 0 is always true"); + assertThat(msgs.get(1)).startsWith("Line 7:").contains(">= 1 is always true"); + } + @Test void aTimerCounterComparedWithOneIsFine() { List msgs = SourceValidator.validateKnownHeaders(""" @@ -491,6 +507,8 @@ void aTimerCounterComparedWithOneIsFine() { simple: "resource:file:${exchangeProperty.CamelTimerCounter == 1 ? 'a.json' : 'b.json'}" - filter: simple: "${exchangeProperty.CamelTimerCounter} > 1" + - filter: + simple: "${exchangeProperty.CamelTimerCounter} >= 2" """, catalog); assertThat(msgs).isEmpty(); } From e0f7dfb3df91678325b679f2fd3cacd61a06c0f4 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 18 Oct 2026 17:26:08 +0200 Subject: [PATCH 3/3] CAMEL-25744: camel-jbang - test that >= 0 on CamelTimerCounter is reported as always true Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../jbang/core/commands/ai/SourceValidatorEndpointTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java index 4fde550d4cf79..b64b7f94305e1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java @@ -491,10 +491,13 @@ void aTimerCounterComparisonThatIsAlwaysTrueIsReported() { simple: "${exchangeProperty.CamelTimerCounter} > 0" - filter: groovy: "exchange.getProperty('CamelTimerCounter', Integer) >= 1" + - filter: + simple: "${exchangeProperty.CamelTimerCounter} >= 0" """, catalog); - assertThat(msgs).hasSize(2); + assertThat(msgs).hasSize(3); assertThat(msgs.get(0)).startsWith("Line 5:").contains("> 0 is always true"); assertThat(msgs.get(1)).startsWith("Line 7:").contains(">= 1 is always true"); + assertThat(msgs.get(2)).startsWith("Line 9:").contains(">= 0 is always true"); } @Test