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 55dde649d405 CAMEL-25953: camel-jbang - AI tools: an unchanged write
says so, the log marks records from before the last reload, and a missed edit
shows the file as it is (#28270)
55dde649d405 is described below
commit 55dde649d4054a1d6bda1fac415e2375461e9d97
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 22 22:15:26 2026 +0200
CAMEL-25953: camel-jbang - AI tools: an unchanged write says so, the log
marks records from before the last reload, and a missed edit shows the file as
it is (#28270)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 14 +-
.../dsl/jbang/core/commands/ai/AuthoringTools.java | 177 +++++++++++++++++++--
.../dsl/jbang/core/commands/ai/LogFileReader.java | 2 +-
.../core/commands/ai/AuthoringToolsEditTest.java | 29 +++-
.../commands/ai/AuthoringToolsLenientTest.java | 54 +++++++
.../jbang/core/commands/ai/AuthoringToolsTest.java | 54 ++++++-
.../jbang/core/commands/ai/LogFileReaderTest.java | 19 +++
7 files changed, 335 insertions(+), 14 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 29ad36b1652f..eb7e7c67263c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2335,8 +2335,18 @@ Content with more than one reading is refused as before.
`camel_edit_file` now matches lines to find that are not in the file as given,
as when a model writes them from memory,
to the closest lines of the file when exactly one place matches well. It
changes there the lines the model changed and
-keeps the file's version of the other lines. The edit stands only when its
result is valid; otherwise the answer is
-the one of a miss, with the lines the file has there. `matchedLines` in the
answer names the lines it matched.
+keeps the file's version of the other lines, and the lines of the file the
model did not have. The edit stands only
+when its result is valid; otherwise the answer is the one of a miss, with the
lines the file has there. `matchedLines`
+in the answer names the lines it matched.
+
+A write of the content the file already has, and an edit whose replacement
gives the text the file has, answer with
+the status `unchanged` instead of `overwritten` or `edited`. Nothing is
written, so a running integration does not
+reload. When one of several edits misses, the answer shows the file as it is
in `fileWindow` and says that none of the
+edits was written, also not the ones before it that matched.
+
+`camel_get_log` marks the records from before the last reload of the routes
with `beforeLastReload`, with a
+`reloadNote` first in the answer. The note of `camel_get_errors` on errors
from before the last reload is now the first
+key of its answer, and says how many exchanges failed since.
=== camel-jbang - the console output of an integration that the AI tools start
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 31174238a18a..043f1e2f196a 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
@@ -356,8 +356,10 @@ public final class AuthoringTools {
// the process selected before is gone, such as one
that died at startup
return previousLog(ctx, args, null).toJson();
}
- return LogFileReader.read(ctx.pid(), p != null ? p.name()
: null, integer(args, "limit", 50),
- args.get("filter"), args.get("level"), bool(args,
"details", false)).toJson();
+ String name = p != null ? p.name() : null;
+ return
markLogBeforeLastReload(LogFileReader.read(ctx.pid(), name, integer(args,
"limit", 50),
+ args.get("filter"), args.get("level"), bool(args,
"details", false)), ctx.pid(), name)
+ .toJson();
}));
registry.accept(tool("camel_get_errors",
@@ -698,7 +700,12 @@ public final class AuthoringTools {
return exact;
}
}
- if (!"invalid".equals(result.getString("status"))) {
+ 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.");
+ } else if (!"invalid".equals(result.getString("status"))) {
result.put("status", "edited");
if (edit.get("matchedLines") instanceof JsonArray matched) {
result.put("matchedLines", matched);
@@ -771,6 +778,7 @@ public final class AuthoringTools {
} catch (IOException e) {
throw new ToolExecutionException("Failed to read " + path + ": " +
e.getMessage());
}
+ String original = content;
JsonObject result = null;
JsonArray lines = new JsonArray();
JsonArray matched = new JsonArray();
@@ -779,11 +787,25 @@ public final class AuthoringTools {
JsonObject one = editedSnippet(file, content, all.get(i)[0],
all.get(i)[1], closest);
if (one.getString("content") == null) {
if (all.size() > 1) {
+ if (i > 0) {
+ // nothing is written, so the window shows the file as
it is: a window with the edits before
+ // made in it was read as those edits written, and
only the one that missed was sent again
+ // (r21 of the local-model benchmark)
+ JsonObject asItIs = editedSnippet(file, original,
all.get(i)[0], all.get(i)[1], closest);
+ if (asItIs.getString("content") == null &&
asItIs.get("fileWindow") != null) {
+ one = asItIs;
+ }
+ }
one.put("edit", i + 1);
one.put("edits", all.size());
+ String earlier = i == 1 ? "edit 1, which matched" : "edits
1 to " + i + ", which matched";
one.put("message", "Edit " + (i + 1) + " of " + all.size()
+ ": " + one.getString("message")
- + (i > 0 ? " (the file as the edits
before it leave it)" : "")
- + ". None of the edits was made: the
file is unchanged.");
+ + ". None of the edits was written, so
the file is unchanged"
+ + (i > 0
+ ? ", also not " + earlier + ":
send all " + all.size() + " edits again in"
+ + " one call, the text to
find of each copied from the file as it is"
+ + " (fileWindow)."
+ : "."));
}
return one;
}
@@ -1207,9 +1229,11 @@ public final class AuthoringTools {
// the depth of the replacement where it goes: from the anchor
before it, or after it for the first
int d = pf >= 0 ? leadingSpaces(lines[fw[pf]]) -
leadingSpaces(f.get(pf))
: a[0] < f.size() ? leadingSpaces(lines[a[1]]) -
leadingSpaces(f.get(a[0])) : firstDelta;
+ List<String> replacement = new ArrayList<>();
for (String line : r.subList(pr + 1, a[2])) {
- out.add(shiftIndent(line, d));
+ replacement.add(shiftIndent(line, d));
}
+ out.addAll(mergeStretch(f.subList(pf + 1, a[0]), lines, pw +
1, a[1], d, replacement));
}
if (a[0] < f.size()) {
out.add(lines[a[1]]);
@@ -1225,6 +1249,47 @@ public final class AuthoringTools {
return new ClosestEdit(start, end, String.join("\n", out) + (newline ?
"\n" : ""), ws + 1, we + 1);
}
+ /**
+ * The lines of a stretch of the file that the model changed: the lines of
the file that find has (the same text, or
+ * the same key at the same depth) are replaced by replace's version,
where the first of them stands, and the lines
+ * of the file that find does not have stay, as a parameter the model did
not remember (r21 of the local-model
+ * benchmark: name: report of a cron endpoint was dropped). When find has
lines here and none of them is in the
+ * file, the model remembered the stretch wrongly, and replace takes its
place.
+ */
+ private static List<String> mergeStretch(
+ List<String> find, String[] lines, int from, int to, int delta,
List<String> replacement) {
+ boolean[] known = new boolean[Math.max(0, to - from)];
+ boolean anyKnown = false;
+ int next = 0;
+ for (String line : find) {
+ for (int k = next; k < known.length; k++) {
+ if (sameLineOrKey(line, lines[from + k], delta)) {
+ known[k] = true;
+ anyKnown = true;
+ next = k + 1;
+ break;
+ }
+ }
+ }
+ if (!find.isEmpty() && !anyKnown) {
+ return replacement;
+ }
+ List<String> out = new ArrayList<>();
+ boolean placed = false;
+ for (int k = 0; k < known.length; k++) {
+ if (!known[k]) {
+ out.add(lines[from + k]);
+ } else if (!placed) {
+ out.addAll(replacement);
+ placed = true;
+ }
+ }
+ if (!placed) {
+ out.addAll(replacement);
+ }
+ return out;
+ }
+
/** Which line of b each line of a is (their longest common subsequence),
or -1; empty lines match nothing. */
static int[] alignLines(List<String> a, List<String> b) {
int[][] t = new int[a.size() + 1][b.size() + 1];
@@ -1622,6 +1687,19 @@ public final class AuthoringTools {
return result;
}
}
+ if (exists && content.equals(read(path))) {
+ // the content the file has: nothing to write and no reload to
wait for. Told "overwritten", a model wrote
+ // one file unchanged up to 24 times in a row (r20 and r21 of the
local-model benchmark)
+ LAST_REFUSED.remove(path.toAbsolutePath().normalize());
+ JsonObject result = new JsonObject();
+ result.put("status", "unchanged");
+ 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.");
+ return result;
+ }
// a client that asks before each write (camel ask, CAMEL-25676) shows
the change first; the others write as
// before
if (ctx.confirmer() != null
@@ -2365,13 +2443,94 @@ public final class AuthoringTools {
}
}
if (before > 0) {
- errors.put("note", before + (before == 1 ? " exchange" : "
exchanges") + " failed before the routes were last"
- + " reloaded (at " + reloadTime + "), so on an
earlier version of the routes"
- + " (beforeLastReload); the errors are kept
until the integration restarts");
+ int since = errors.get("errors") instanceof List<?> entries ?
entries.size() - before : 0;
+ // first in the answer: the errors before it carry their stack
traces, and a model that reads the first
+ // few thousand characters of a 60 KB answer never got to a note
at the end (r21 of the local-model
+ // benchmark)
+ putFirst(errors, "note", before + (before == 1 ? " exchange" : "
exchanges")
+ + " failed before the routes were last
reloaded (at " + reloadTime + "), so on an"
+ + " earlier version of the routes
(beforeLastReload), "
+ + (since == 0 ? "and none since" : since
+ " since")
+ + "; the errors are kept until the
integration restarts");
}
return errors;
}
+ /** Puts a key first in an answer, in place: what is read first is the
start of the answer. */
+ static void putFirst(JsonObject answer, String key, Object value) {
+ JsonObject rest = new JsonObject();
+ rest.putAll(answer);
+ rest.remove(key);
+ answer.clear();
+ answer.put(key, value);
+ answer.putAll(rest);
+ }
+
+ /**
+ * Marks the log records of an earlier version of the routes: those before
the newest reload of the routes in the
+ * log. A model that read the ERROR records of camel_get_log after a
change took the ones from before its change for
+ * errors of its new version, and changed a route that worked (r21 of the
local-model benchmark).
+ */
+ static JsonObject markLogBeforeLastReload(JsonObject log, long pid, String
name) {
+ try {
+ JsonObject reloads = LogFileReader.read(pid, name, 1,
ReloadOutcome.RELOADED, null);
+ if (reloads.get("lines") instanceof List<?> lines &&
!lines.isEmpty()
+ && lines.get(0) instanceof JsonObject reload) {
+ return markLogBefore(log, reload.getString("time"));
+ }
+ } catch (Exception e) {
+ // no log to tell: the records as they are
+ }
+ return log;
+ }
+
+ /** Marks the records before the reload at the given time of day; the log
keeps the time of day only. */
+ static JsonObject markLogBefore(JsonObject log, String reloadTime) {
+ long reload;
+ try {
+ // the summary is logged after the new routes started: a margin
keeps a route that fails at once unmarked
+ reload = LocalTime.parse(reloadTime).toNanoOfDay() -
RELOAD_MARGIN_MILLIS * 1_000_000L;
+ } catch (Exception e) {
+ return log;
+ }
+ long day = 24L * 3600 * 1_000_000_000L;
+ int before = 0;
+ int since = 0;
+ if (log.get("lines") instanceof List<?> rows) {
+ for (Object o : rows) {
+ if (!(o instanceof JsonObject r)) {
+ continue;
+ }
+ long time;
+ try {
+ time = LocalTime.parse(r.getString("time")).toNanoOfDay();
+ } catch (Exception e) {
+ continue;
+ }
+ // a log that runs past midnight: the nearer of the two days
+ long ahead = reload - time;
+ if (ahead > day / 2) {
+ ahead -= day;
+ } else if (ahead < -day / 2) {
+ ahead += day;
+ }
+ if (ahead > 0) {
+ r.put("beforeLastReload", true);
+ before++;
+ } else {
+ since++;
+ }
+ }
+ }
+ if (before > 0) {
+ putFirst(log, "reloadNote", before + (before == 1 ? " record is" :
" records are")
+ + " from before the routes were last
reloaded (at " + reloadTime + "), so from"
+ + " an earlier version of the routes
(beforeLastReload)"
+ + (since == 0 ? "; none is from the
current version" : ""));
+ }
+ return log;
+ }
+
/**
* The .camel folders under the directory, with the number of files in
each: a file consumer moves a file it read
* into .camel (its default, without noop=true), so a folder whose files
were all read looked empty.
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
index f67c1dc3cd8e..3390211ec021 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReader.java
@@ -48,7 +48,7 @@ public final class LogFileReader {
"^(\\d{4}-\\d{2}-\\d{2})[T ](\\d{2}:\\d{2}:\\d{2}\\.\\d+)\\S*\\s+"
+
"(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\\s+"
+
"\\d+\\s+---\\s+"
- +
"\\[([^]]*)]\\s+"
+ +
"\\[((?:[^\\[\\]]|\\[[^\\]]*])*)]\\s+"
+
"(\\S+)\\s*:\\s*(.*)$");
private static final Pattern ANSI = Pattern.compile("\\u001b\\[[;\\d]*[
-/]*[@-~]");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
index 2784e3791b58..e889e69f975a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
@@ -269,10 +269,37 @@ class AuthoringToolsEditTest {
assertThat(result.getString("status")).isEqualTo("not-found");
assertThat(result.getInteger("edit")).isEqualTo(2);
assertThat(result.getString("message")).startsWith("Edit 2 of 2: ")
- .endsWith("None of the edits was made: the file is
unchanged.");
+ .endsWith("None of the edits was written, so the file is
unchanged, also not edit 1, which matched: send"
+ + " all 2 edits again in one call, the text to find
of each copied from the file as it is"
+ + " (fileWindow).");
+ // the window is the file as it is, without edit 1 in it: a window
with edit 1 made in it was read as edit 1
+ // written, and only edit 2 was sent again (r21 of the local-model
benchmark)
+ assertThat(result.getString("fileWindow")).contains("message:
\"one\"").doesNotContain("uno");
assertThat(Files.readString(dir.resolve("demo.camel.yaml"))).isEqualTo(ROUTE);
}
+ /** A write of the content the file has: nothing is written, and the
answer says so. */
+ @Test
+ void aWriteOfTheContentTheFileHasIsUnchanged(@TempDir Path dir) throws
IOException {
+ Path file = dir.resolve("demo.camel.yaml");
+ Files.writeString(file, ROUTE);
+ java.nio.file.attribute.FileTime before =
java.nio.file.attribute.FileTime.fromMillis(1_000_000L);
+ Files.setLastModifiedTime(file, before);
+
+ JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir,
"demo.camel.yaml", ROUTE, true);
+
+
assertThat(result.getString("status")).as(result.toJson()).isEqualTo("unchanged");
+ assertThat(result.getString("message")).startsWith("The file already
has exactly this content, so nothing was"
+ + " written and
nothing reloads.");
+ assertThat(Files.getLastModifiedTime(file)).as("not written: no
reload").isEqualTo(before);
+
+ // an edit whose replacement is the text the file has
+ JsonObject edit = edit(dir, "message: \"one\"", "message: \"one\"");
+
assertThat(edit.getString("status")).as(edit.toJson()).isEqualTo("unchanged");
+ assertThat(edit.getString("message")).startsWith("The edit leaves the
file as it is");
+ assertThat(Files.getLastModifiedTime(file)).isEqualTo(before);
+ }
+
@Test
void editsSentAsAStringHoldingJsonAreRead(@TempDir Path dir) throws
IOException {
Files.writeString(dir.resolve("demo.camel.yaml"), ROUTE);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
index 0dad73019998..c3e0af46da87 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
@@ -488,6 +488,60 @@ class AuthoringToolsLenientTest {
.contains("message: \"archived\"");
}
+ /**
+ * A line of the file that the snippet from memory does not have stays:
the model remembered the cron endpoint
+ * without its name parameter, and the merge dropped name: report (r21 of
the local-model benchmark).
+ */
+ @Test
+ void aLineOfTheFileTheSnippetDoesNotHaveStays(@TempDir Path dir) throws
IOException {
+ String route = """
+ - route:
+ id: nightly-report
+ from:
+ uri: cron
+ parameters:
+ name: report
+ schedule: "0/10 * * * * ?"
+ steps:
+ - log:
+ message: "Inventory report: 120 T-shirts and 45
mugs in stock"
+ """;
+ Files.writeString(dir.resolve(FILE), route);
+ JsonObject result = AuthoringTools.editFile(new ToolContext(), dir,
FILE, """
+ uri: cron:report
+ parameters:
+ schedule: "0/10 * * * * ?"
+ """, """
+ uri: cron:report
+ parameters:
+ schedule: "{{report.schedule}}"
+ """);
+
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("edited");
+
assertThat(Files.readString(dir.resolve(FILE))).isEqualTo(route.replace("\"0/10
* * * * ?\"",
+ "\"{{report.schedule}}\""));
+ }
+
+ /** A line the model adds between two lines it remembers goes in, and the
file's line between them stays. */
+ @Test
+ void aLineAddedBetweenTwoRememberedLinesKeepsTheFilesLineThere(@TempDir
Path dir) throws IOException {
+ Files.writeString(dir.resolve(FILE), ROUTE);
+ JsonObject result = AuthoringTools.editFile(new ToolContext(), dir,
FILE, """
+ - unmarshal:
+ json:
+ - log:
+ message: "Invoice ${body[invoiceId]} for
${body[orderId]}"
+ """, """
+ - unmarshal:
+ json:
+ allowUnmarshallType: true
+ - log:
+ message: "Invoice ${body[invoiceId]} for
${body[orderId]}"
+ """);
+
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("edited");
+ String after = Files.readString(dir.resolve(FILE));
+ assertThat(after).contains("library:
Jackson").contains("allowUnmarshallType: true");
+ }
+
@Test
void aClosestMatchWithAnInvalidResultIsNotFound(@TempDir Path dir) throws
IOException {
Files.writeString(dir.resolve(FILE), ROUTE);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
index e4bf42b3b783..61099db11db6 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsTest.java
@@ -194,8 +194,60 @@ class AuthoringToolsTest {
assertFalse(entries.get(1).containsKey("beforeLastReload"), "within a
second of the reload: maybe the new routes");
assertFalse(entries.get(2).containsKey("beforeLastReload"), "after the
reload");
assertEquals("1 exchange failed before the routes were last reloaded
(at 08:18:09.731), so on an earlier version"
- + " of the routes (beforeLastReload); the errors are kept
until the integration restarts",
+ + " of the routes (beforeLastReload), 2 since; the errors
are kept until the integration restarts",
errors.getString("note"));
+ assertEquals("note", errors.keySet().iterator().next(), "the note
comes before the errors and their stack traces");
+ }
+
+ /** camel_get_log marks the records from before the last reload, with a
note first in the answer. */
+ @Test
+ void theLogRecordsFromBeforeTheLastReloadAreMarked() {
+ JsonObject log = new JsonObject();
+ JsonArray lines = new JsonArray();
+ for (String time : new String[] { "08:20:00.000", "08:18:09.500",
"08:10:00.000" }) {
+ JsonObject r = new JsonObject();
+ r.put("time", time);
+ r.put("level", "ERROR");
+ lines.add(r);
+ }
+ log.put("lines", lines);
+ log.put("returnedLines", 3);
+
+ AuthoringTools.markLogBefore(log, "08:18:09.731");
+
+ List<JsonObject> rows = entries(log, "lines");
+ assertFalse(rows.get(0).containsKey("beforeLastReload"), "after the
reload");
+ assertFalse(rows.get(1).containsKey("beforeLastReload"), "within a
second of the reload: maybe the new routes");
+ assertEquals(Boolean.TRUE, rows.get(2).get("beforeLastReload"));
+ assertEquals("reloadNote", log.keySet().iterator().next());
+ assertEquals("1 record is from before the routes were last reloaded
(at 08:18:09.731), so from an earlier version"
+ + " of the routes (beforeLastReload)",
+ log.getString("reloadNote"));
+
+ // a log that runs past midnight: 23:59:50 is before a reload at
00:00:30, and 00:00:40 is after it
+ JsonObject night = new JsonObject();
+ JsonArray nightLines = new JsonArray();
+ for (String time : new String[] { "00:00:40.000", "23:59:50.000" }) {
+ JsonObject r = new JsonObject();
+ r.put("time", time);
+ nightLines.add(r);
+ }
+ night.put("lines", nightLines);
+ AuthoringTools.markLogBefore(night, "00:00:30.000");
+ List<JsonObject> nightRows = entries(night, "lines");
+ assertFalse(nightRows.get(0).containsKey("beforeLastReload"));
+ assertEquals(Boolean.TRUE, nightRows.get(1).get("beforeLastReload"));
+
+ // no record before the reload: no note
+ JsonObject fresh = new JsonObject();
+ fresh.put("lines", new JsonArray(List.of(new JsonObject(Map.of("time",
"08:20:00.000")))));
+ AuthoringTools.markLogBefore(fresh, "08:18:09.731");
+ assertFalse(fresh.containsKey("reloadNote"));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List<JsonObject> entries(JsonObject answer, String key) {
+ return List.copyOf((java.util.Collection<JsonObject>) answer.get(key));
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
index 109a110ed1e8..a6dbbf18b98f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/LogFileReaderTest.java
@@ -52,6 +52,25 @@ class LogFileReaderTest {
assertNull(rows.get(0).get("detail"));
}
+ /**
+ * A thread name with brackets, such as the JMS consumer's
[onsumer[orders]], is a record of its own: it was folded
+ * into the record before it, so the errors of one route showed as the
cause of another (r21 of the local-model
+ * benchmark).
+ */
+ @Test
+ void aThreadNameWithBracketsStartsARecord() {
+ List<String> lines = List.of(
+ "2026-10-22 01:25:59.501 INFO 93557 --- [ - file://orders]
jms-request-reply.camel.yaml:20 : Order ORD-1003 put on the queue",
+ "2026-10-22 01:25:59.530 WARN 93557 --- [onsumer[orders]]
jms-request-reply.camel.yaml:30 : Reserving ORD-1003 failed");
+ List<JsonObject> rows
+ = List.copyOf(LogFileReader.build(lines, 50, null, null, new
JsonObject()).getCollection("lines"));
+ assertEquals(2, rows.size());
+ assertEquals("Reserving ORD-1003 failed",
rows.get(0).getString("message"));
+ assertEquals("WARN", rows.get(0).getString("level"));
+ assertEquals("Order ORD-1003 put on the queue",
rows.get(1).getString("message"));
+ assertNull(rows.get(1).get("detail"));
+ }
+
@Test
void foldsAStormOfTheSameLineIntoOneRecord() {
List<String> storm = new java.util.ArrayList<>();