This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25953 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 5212617ccd6be47665e5854e53b890c376584eae Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 22 08:21:47 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 From the local-model benchmark of Oct 21-22 2026: - a write of the content the file has, and an edit that gives the text the file has, answer status unchanged and write nothing, so a running integration does not reload - camel_get_log marks the records from before the last reload (beforeLastReload) with a reloadNote first; the note of camel_get_errors comes first and says how many exchanges failed since - camel_get_log reads a thread name with brackets ([onsumer[orders]]) instead of folding its records into the record before - when one of several edits misses, fileWindow shows the file as it is and the message says to send all the edits again - the closest-lines merge keeps the file's lines that the snippet from memory does not have Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../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 46379cf15944..2635c4e5af82 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 @@ -2322,8 +2322,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<>();
