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 2b8227a44ce7 CAMEL-24397: camel-tui - Fix --record producing no cast 
file
2b8227a44ce7 is described below

commit 2b8227a44ce7c7f994489b95b76fa521e0af37ad
Author: Adriano Machado <[email protected]>
AuthorDate: Mon Aug 24 08:15:08 2026 -0400

    CAMEL-24397: camel-tui - Fix --record producing no cast file
    
    TamboUI only applies its recording wrapper in BackendFactory.create(),
    but TuiBackendHelper always supplies an explicit JLineBackend, so that
    factory path — and recording — never ran. --record exited cleanly
    without replaying a tape or writing a .cast file.
    
    Apply the recording wrapper explicitly in TuiBackendHelper instead of
    relying on auto-discovery (which stays disabled here to avoid picking
    an Aesh backend that doesn't shut down cleanly under --web). Replace
    the hardcoded 200x50 recording geometry with --record-size,
    --record-fps and --record-duration, forwarded from TuiCommand (the
    plugin's actual entry point). Reject --record combined with --web,
    since the underlying tamboui.record* properties are process-wide and
    every browser session would otherwise share one cast file.
    
    Closes #25514
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   7 +
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  29 ++++
 dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml     |   6 +
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 106 +++++++++++--
 .../jbang/core/commands/tui/TuiBackendHelper.java  |  24 ++-
 .../dsl/jbang/core/commands/tui/TuiCommand.java    |  41 ++++-
 .../tui/CamelMonitorRecordOptionsTest.java         | 122 +++++++++++++++
 .../tui/TuiBackendHelperRecordingTest.java         | 168 +++++++++++++++++++++
 .../commands/tui/TuiCommandRecordOptionsTest.java  |  76 ++++++++++
 9 files changed, 565 insertions(+), 14 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 7de03cbc6c53..ee297b9db3c3 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -85,6 +85,13 @@ The same default is now also applied to the `ClientConfig` 
that Camel builds for
 endpoints, when neither a referenced `ClientConfig` nor `hazelcastConfigUri` 
is supplied. Client mode
 previously behaved differently from node mode for an otherwise identical 
endpoint configuration.
 
+=== camel-jbang (TUI)
+
+`camel tui --record` is now rejected when combined with `--web`. The recording 
configuration applies
+to the whole process, so a browser session served by `--web` would be recorded 
into the same `.cast`
+file as the local session. Previously the combination was accepted, but 
recording never produced any
+output, so run the two modes in separate processes instead.
+
 === camel-mail
 
 `MimeMultipartDataFormat` now uses `MailHeaderFilterStrategy` instead of a 
plain
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index 9c7192eab078..eac4a6e96dc5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -892,6 +892,23 @@ https://asciinema.org/[Asciinema] `.cast` recording:
 camel tui --record=demo.tape
 ----
 
+The `.cast` file is written next to the tape, with the `.tape` suffix replaced 
by `.cast`.
+Recording is headless: the TUI is driven entirely by the tape rather than by 
your terminal,
+so no keyboard input is read and nothing is drawn on screen.
+
+The recorded terminal is 200x50 by default, which is wider than a 
documentation page can
+display. Use `--record-size` to record at a size that fits, and `--record-fps` 
or
+`--record-duration` to control the capture rate and the cut-off:
+
+[source,bash]
+----
+camel tui --record=demo.tape --record-size=160x44 --record-fps=15
+----
+
+`--record` cannot be combined with `--web`. Recording drives a headless TUI 
from the tape, and
+the recording configuration applies to the whole process, so every browser 
session would be
+recorded into the same `.cast` file. Camel rejects the combination with an 
error instead.
+
 === Converting to GIF
 
 Convert recordings using https://github.com/asciinema/agg[agg] (for `.cast` 
files) or
@@ -940,4 +957,16 @@ vhs demo.tape               # .tape -> .gif
 | `--record`
 | Replay a `.tape` file and record the session to an Asciinema `.cast` file.
 |
+
+| `--record-size`
+| Size of the recorded terminal for `--record`, as `<cols>x<rows>`.
+| `200x50`
+
+| `--record-fps`
+| Frames per second captured by `--record`.
+| `10`
+
+| `--record-duration`
+| Maximum duration in milliseconds captured by `--record`.
+| `120000`
 |===
diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml 
b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
index 241adb18dbad..9882e20e78a9 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/pom.xml
@@ -130,6 +130,12 @@
             <version>${awaitility-version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.junit-pioneer</groupId>
+            <artifactId>junit-pioneer</artifactId>
+            <version>${junit-pioneer-version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 92fdfaebe6c9..197aa3e99377 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.Queue;
 import java.util.concurrent.CompletableFuture;
@@ -82,6 +83,19 @@ public class CamelMonitor extends CamelCommand {
     private static final Logger LOG = 
System.getLogger(CamelMonitor.class.getName());
     private static final long DEFAULT_REFRESH_MS = 500;
 
+    /**
+     * The TamboUI system properties {@code --record} configures. They are 
process-wide, so the session that sets them
+     * clears them again on the way out; otherwise a later TUI backend created 
in the same JVM would still see recording
+     * enabled via {@code RecordingConfig.isEnabled()}.
+     */
+    static final List<String> RECORD_PROPERTIES = List.of(
+            "tamboui.record",
+            "tamboui.record.config",
+            "tamboui.record.width",
+            "tamboui.record.height",
+            "tamboui.record.duration",
+            "tamboui.record.fps");
+
     // Compact tab bar (10 labels + 9 "|" dividers) needs 88 chars — that is 
the true minimum
     private static final int MIN_WIDTH = 88;
     private static final int MIN_HEIGHT = 24;
@@ -101,6 +115,21 @@ public class CamelMonitor extends CamelCommand {
                         arity = "0..1")
     String record;
 
+    @CommandLine.Option(names = { "--record-size" },
+                        description = "Size of the recorded terminal for 
--record, as <cols>x<rows> (default: ${DEFAULT-VALUE})",
+                        defaultValue = "200x50")
+    String recordSize = "200x50";
+
+    @CommandLine.Option(names = { "--record-fps" },
+                        description = "Frames per second captured by --record 
(default: ${DEFAULT-VALUE})",
+                        defaultValue = "10")
+    int recordFps = 10;
+
+    @CommandLine.Option(names = { "--record-duration" },
+                        description = "Maximum duration in milliseconds 
captured by --record (default: ${DEFAULT-VALUE})",
+                        defaultValue = "120000")
+    int recordDuration = 120000;
+
     @CommandLine.Option(names = { "--mcp" },
                         description = "Enable embedded MCP server for AI agent 
access to the TUI")
     boolean mcp;
@@ -184,6 +213,68 @@ public class CamelMonitor extends CamelCommand {
         this.classLoader = classLoader;
     }
 
+    /**
+     * Parses a {@code --record-size} value such as {@code 160x44} into {@code 
[cols, rows]}.
+     */
+    int[] parseRecordSize(String size) {
+        String[] parts = size == null ? new String[0] : 
size.toLowerCase(Locale.ROOT).split("x", -1);
+        if (parts.length == 2) {
+            try {
+                int cols = Integer.parseInt(parts[0].trim());
+                int rows = Integer.parseInt(parts[1].trim());
+                if (cols > 0 && rows > 0) {
+                    return new int[] { cols, rows };
+                }
+            } catch (NumberFormatException e) {
+                // fall through to the parameter error below
+            }
+        }
+        throw new CommandLine.ParameterException(
+                new CommandLine(this),
+                "Invalid value for option '--record-size': expected 
'<cols>x<rows>' with positive numbers, was '"
+                                       + size + "'");
+    }
+
+    /**
+     * Hands the {@code --record*} options to TamboUI through the {@link 
#RECORD_PROPERTIES} system properties, which is
+     * the only way TamboUI accepts a recording configuration.
+     */
+    void configureRecording() {
+        if (record == null) {
+            return;
+        }
+        if (web) {
+            // The properties below are process-wide, so every browser session 
spawned by TuiWebServer would be
+            // wrapped for recording too, all writing the same cast file. The 
two modes are also conceptually
+            // exclusive: --record drives a headless TUI from a tape rather 
than from a connected terminal.
+            throw new CommandLine.ParameterException(
+                    new CommandLine(this),
+                    "Option '--record' cannot be combined with '--web': 
recording replays a tape headlessly "
+                                           + "and would be inherited by every 
browser session");
+        }
+        Path tapeFile = Path.of(record);
+        Path castFile = Path.of(record.replaceAll("\\.tape$", "") + ".cast");
+        int[] size = parseRecordSize(recordSize);
+        System.setProperty("tamboui.record", 
castFile.toAbsolutePath().toString());
+        System.setProperty("tamboui.record.config", 
tapeFile.toAbsolutePath().toString());
+        System.setProperty("tamboui.record.width", String.valueOf(size[0]));
+        System.setProperty("tamboui.record.height", String.valueOf(size[1]));
+        System.setProperty("tamboui.record.duration", 
String.valueOf(recordDuration));
+        System.setProperty("tamboui.record.fps", String.valueOf(recordFps));
+    }
+
+    /**
+     * Undoes {@link #configureRecording()} at the end of the session that ran 
it.
+     * <p>
+     * The already-loaded {@code RecordingConfig} keeps its own copy, so the 
shutdown hook still writes the cast file;
+     * clearing only stops a TUI backend created later in the same JVM from 
being wrapped for recording again.
+     */
+    void clearRecordingProperties() {
+        for (String key : RECORD_PROPERTIES) {
+            System.clearProperty(key);
+        }
+    }
+
     @Override
     public Integer doCall() throws Exception {
         System.setProperty("java.awt.headless", "true");
@@ -209,16 +300,7 @@ public class CamelMonitor extends CamelCommand {
         }
 
         // Configure TamboUI recording if --record is specified
-        if (record != null) {
-            Path tapeFile = Path.of(record);
-            Path castFile = Path.of(record.replaceAll("\\.tape$", "") + 
".cast");
-            System.setProperty("tamboui.record", 
castFile.toAbsolutePath().toString());
-            System.setProperty("tamboui.record.config", 
tapeFile.toAbsolutePath().toString());
-            System.setProperty("tamboui.record.width", "200");
-            System.setProperty("tamboui.record.height", "50");
-            System.setProperty("tamboui.record.duration", "120000");
-            System.setProperty("tamboui.record.fps", "10");
-        }
+        configureRecording();
 
         recordingManager.init(record != null);
 
@@ -649,6 +731,10 @@ public class CamelMonitor extends CamelCommand {
             }
             deleteMcpJson(mcpJsonFile);
             this.runner = null;
+            if (record != null) {
+                // Only the session that set the properties clears them again
+                clearRecordingProperties();
+            }
         }
         return 0;
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
index d3abe4a7cda2..bbb0b052957f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelper.java
@@ -17,6 +17,8 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import dev.tamboui.backend.jline3.JLineBackend;
+import dev.tamboui.internal.record.RecordingBackend;
+import dev.tamboui.internal.record.RecordingConfig;
 import dev.tamboui.terminal.Backend;
 import dev.tamboui.tui.TuiConfig;
 import dev.tamboui.tui.TuiRunner;
@@ -35,10 +37,28 @@ final class TuiBackendHelper {
         // classpath (for --web), auto-discovery can pick AeshBackend for the 
local session too,
         // which drives a native PosixSysTerminal that doesn't shut down 
cleanly here.
         JLineBackend backend = activeTerminal != null ? new 
JLineBackend(activeTerminal) : new JLineBackend();
-        return 
TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build());
+        return createTuiRunner(backend);
     }
 
     static TuiRunner createTuiRunner(Backend backend) throws Exception {
-        return 
TuiRunner.create(TuiConfig.builder().backend(backend).mouseCapture(true).build());
+        return 
TuiRunner.create(TuiConfig.builder().backend(applyRecording(backend)).mouseCapture(true).build());
+    }
+
+    /**
+     * Wraps the backend for Asciinema recording when {@code --record} 
configured the {@code tamboui.record*} system
+     * properties.
+     * <p>
+     * TamboUI normally does this inside {@code BackendFactory.create()}, but 
{@code TuiRunner} only calls that factory
+     * when no explicit backend is configured. Because we must pass an 
explicit backend (see
+     * {@link #createTuiRunner()}), the wrapping has to be done here instead, 
otherwise {@code --record} exits cleanly
+     * without replaying the tape or writing a {@code .cast} file.
+     */
+    static Backend applyRecording(Backend backend) {
+        // Guard on isEnabled() first: load() caches its result process-wide 
and installs a System.out capture
+        if (!RecordingConfig.isEnabled()) {
+            return backend;
+        }
+        RecordingConfig config = RecordingConfig.load();
+        return config != null ? new RecordingBackend(backend, config) : 
backend;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
index b5e0b9fc3f80..fdbc5c91e531 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommand.java
@@ -59,6 +59,21 @@ public class TuiCommand extends CamelCommand {
                         arity = "0..1")
     String record;
 
+    @CommandLine.Option(names = { "--record-size" },
+                        description = "Size of the recorded terminal for 
--record, as <cols>x<rows> (default: ${DEFAULT-VALUE})",
+                        defaultValue = "200x50")
+    String recordSize = "200x50";
+
+    @CommandLine.Option(names = { "--record-fps" },
+                        description = "Frames per second captured by --record 
(default: ${DEFAULT-VALUE})",
+                        defaultValue = "10")
+    int recordFps = 10;
+
+    @CommandLine.Option(names = { "--record-duration" },
+                        description = "Maximum duration in milliseconds 
captured by --record (default: ${DEFAULT-VALUE})",
+                        defaultValue = "120000")
+    int recordDuration = 120000;
+
     @CommandLine.Option(names = { "--theme" },
                         description = "Color theme: dark or light (overrides 
persisted preference for this session)",
                         completionCandidates = 
ThemeModeCompletionCandidates.class)
@@ -71,6 +86,17 @@ public class TuiCommand extends CamelCommand {
 
     @Override
     public Integer doCall() throws Exception {
+        CamelMonitor cmd = new CamelMonitor(getMain(), classLoader);
+        return new 
CommandLine(cmd).execute(buildArgs().toArray(String[]::new));
+    }
+
+    /**
+     * Builds the {@link CamelMonitor} command line this command delegates to.
+     * <p>
+     * Every option declared here must be forwarded, otherwise the option is 
silently accepted and then ignored. Only
+     * non-default values are passed on, so the delegate keeps applying its 
own defaults.
+     */
+    List<String> buildArgs() {
         List<String> args = new ArrayList<>();
         if (name != null) {
             args.add(name);
@@ -97,11 +123,22 @@ public class TuiCommand extends CamelCommand {
             args.add("--record");
             args.add(record);
         }
+        if (!"200x50".equals(recordSize)) {
+            args.add("--record-size");
+            args.add(recordSize);
+        }
+        if (recordFps != 10) {
+            args.add("--record-fps");
+            args.add(String.valueOf(recordFps));
+        }
+        if (recordDuration != 120000) {
+            args.add("--record-duration");
+            args.add(String.valueOf(recordDuration));
+        }
         if (theme != null) {
             args.add("--theme");
             args.add(theme);
         }
-        CamelMonitor cmd = new CamelMonitor(getMain(), classLoader);
-        return new CommandLine(cmd).execute(args.toArray(String[]::new));
+        return args;
     }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorRecordOptionsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorRecordOptionsTest.java
new file mode 100644
index 000000000000..9b47a877c040
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitorRecordOptionsTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.tui;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.ClearSystemProperty;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for the {@code --record*} options.
+ * <p>
+ * The recording geometry used to be hardcoded at 200x50, which is too wide to 
embed in a documentation page, so it is
+ * now configurable. A bad value must be rejected up front with a clear 
message rather than silently recording at the
+ * wrong size, because the mistake would otherwise only surface once the cast 
is rendered.
+ */
+// The options are handed to TamboUI through process-wide system properties, 
so the tests that exercise them have to
+// leave the JVM as they found it; junit-pioneer restores the original values 
after each test.
+@ClearSystemProperty(key = "tamboui.record")
+@ClearSystemProperty(key = "tamboui.record.config")
+@ClearSystemProperty(key = "tamboui.record.width")
+@ClearSystemProperty(key = "tamboui.record.height")
+@ClearSystemProperty(key = "tamboui.record.duration")
+@ClearSystemProperty(key = "tamboui.record.fps")
+class CamelMonitorRecordOptionsTest {
+
+    private final CamelMonitor monitor = new CamelMonitor(new 
CamelJBangMain(), getClass().getClassLoader());
+
+    @Test
+    void parsesColumnsAndRows() {
+        assertThat(monitor.parseRecordSize("160x44")).containsExactly(160, 44);
+    }
+
+    @Test
+    void acceptsUppercaseSeparatorAndSurroundingSpaces() {
+        assertThat(monitor.parseRecordSize(" 160 X 44 ")).containsExactly(160, 
44);
+    }
+
+    @Test
+    void rejectsNonPositiveDimensions() {
+        assertThatThrownBy(() -> monitor.parseRecordSize("0x44"))
+                .isInstanceOf(CommandLine.ParameterException.class)
+                .hasMessageContaining("--record-size");
+    }
+
+    @Test
+    void rejectsMalformedValues() {
+        assertThatThrownBy(() -> monitor.parseRecordSize("160"))
+                .isInstanceOf(CommandLine.ParameterException.class)
+                .hasMessageContaining("--record-size");
+        assertThatThrownBy(() -> monitor.parseRecordSize("wide x tall"))
+                .isInstanceOf(CommandLine.ParameterException.class)
+                .hasMessageContaining("--record-size");
+    }
+
+    @Test
+    void defaultsKeepTheHistoricRecordingGeometry() {
+        // 200x50 was the previously hardcoded value; keeping it as the 
default means this change adds
+        // an override without altering the output of an existing command line.
+        
assertThat(monitor.parseRecordSize(monitor.recordSize)).containsExactly(200, 
50);
+        assertThat(monitor.recordFps).isEqualTo(10);
+        assertThat(monitor.recordDuration).isEqualTo(120000);
+    }
+
+    @Test
+    void rejectsRecordingCombinedWithTheWebTerminal() {
+        // tamboui.record* is JVM-wide, so a browser session started by 
TuiWebServer would silently inherit the
+        // local session's recording and write to the same cast file. Failing 
up front beats that surprise.
+        monitor.record = "demo.tape";
+        monitor.web = true;
+
+        assertThatThrownBy(monitor::configureRecording)
+                .isInstanceOf(CommandLine.ParameterException.class)
+                .hasMessageContaining("--record")
+                .hasMessageContaining("--web");
+    }
+
+    @Test
+    void configuringRecordingSetsEveryPropertyThatIsClearedAgainAfterwards() {
+        // The set and the clear list have to stay in sync: a property added 
to configureRecording() but missing
+        // from RECORD_PROPERTIES would keep recording enabled for the rest of 
the JVM's life.
+        monitor.record = "demo.tape";
+        monitor.recordSize = "160x44";
+
+        monitor.configureRecording();
+
+        assertThat(CamelMonitor.RECORD_PROPERTIES)
+                .allSatisfy(key -> 
assertThat(System.getProperty(key)).as(key).isNotNull());
+        
assertThat(System.getProperty("tamboui.record.width")).isEqualTo("160");
+        
assertThat(System.getProperty("tamboui.record.height")).isEqualTo("44");
+
+        monitor.clearRecordingProperties();
+
+        assertThat(CamelMonitor.RECORD_PROPERTIES)
+                .allSatisfy(key -> 
assertThat(System.getProperty(key)).as(key).isNull());
+    }
+
+    @Test
+    void leavesTheRecordingPropertiesAloneWithoutTheRecordOption() {
+        monitor.configureRecording();
+
+        assertThat(CamelMonitor.RECORD_PROPERTIES)
+                .allSatisfy(key -> 
assertThat(System.getProperty(key)).as(key).isNull());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
new file mode 100644
index 000000000000..fa2e495ae974
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.tui;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import dev.tamboui.buffer.DiffResult;
+import dev.tamboui.internal.record.AnsiTerminalCapture;
+import dev.tamboui.layout.Position;
+import dev.tamboui.layout.Size;
+import dev.tamboui.terminal.Backend;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junitpioneer.jupiter.ClearSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@code camel tui --record} actually engages TamboUI's 
recording backend.
+ * <p>
+ * TamboUI only wraps a backend for recording inside {@code 
BackendFactory.create()}, and {@code TuiRunner} calls that
+ * factory <em>only</em> when no explicit backend is configured. Since 
camel-tui must supply an explicit
+ * {@link dev.tamboui.backend.jline3.JLineBackend} (auto-discovery would 
otherwise pick the Aesh backend that is on the
+ * classpath for {@code --web}), the recording wrapper is never applied unless 
camel-tui applies it itself. When that
+ * wrapping is missing, {@code --record} replays no tape and writes no {@code 
.cast} file, yet still exits cleanly, so
+ * only a test like this one catches the regression.
+ */
+// RecordingConfig.load() also reads fps and duration, so every key camel-tui 
sets has to be managed here: a value
+// leaking out of this class would silently reconfigure recording in an 
unrelated test. junit-pioneer clears each key
+// before the test and restores the original value afterwards, which also 
covers values the test body sets itself.
+@ClearSystemProperty(key = "tamboui.record")
+@ClearSystemProperty(key = "tamboui.record.config")
+@ClearSystemProperty(key = "tamboui.record.width")
+@ClearSystemProperty(key = "tamboui.record.height")
+@ClearSystemProperty(key = "tamboui.record.duration")
+@ClearSystemProperty(key = "tamboui.record.fps")
+class TuiBackendHelperRecordingTest {
+
+    @TempDir
+    Path tempDir;
+
+    @AfterEach
+    void tearDown() {
+        // load() installs a System.out capture; restore the real stream so 
other tests are unaffected
+        if (AnsiTerminalCapture.isInstalled()) {
+            AnsiTerminalCapture.uninstall();
+        }
+    }
+
+    @Test
+    void withoutRecordOptionTheBackendIsHandedToTuiRunnerUntouched() {
+        Backend original = new NoopBackend();
+
+        Backend result = TuiBackendHelper.applyRecording(original);
+
+        assertThat(result).isSameAs(original);
+    }
+
+    @Test
+    void withRecordOptionTheBackendIsWrappedAndSizedFromTheRecordingConfig() 
throws Exception {
+        Path tape = tempDir.resolve("demo.tape");
+        Files.writeString(tape, "Sleep 100ms\nType \"q\"\n");
+        System.setProperty("tamboui.record", 
tempDir.resolve("demo.cast").toString());
+        System.setProperty("tamboui.record.config", tape.toString());
+        System.setProperty("tamboui.record.width", "120");
+        System.setProperty("tamboui.record.height", "30");
+
+        Backend result = TuiBackendHelper.applyRecording(new NoopBackend());
+
+        // A recording backend reports the configured cast dimensions rather 
than the real terminal
+        // size; asserting on those proves the configuration was applied, not 
merely that some
+        // wrapper was returned.
+        assertThat(result).isNotInstanceOf(NoopBackend.class);
+        assertThat(result.size()).isEqualTo(new Size(120, 30));
+    }
+
+    /**
+     * Minimal {@link Backend} stand-in. Only {@link #size()} needs a 
meaningful value, so that the test fails if the
+     * recording dimensions are taken from the delegate instead of the config.
+     */
+    private static final class NoopBackend implements Backend {
+
+        @Override
+        public void draw(DiffResult diff) throws IOException {
+        }
+
+        @Override
+        public void flush() throws IOException {
+        }
+
+        @Override
+        public void clear() throws IOException {
+        }
+
+        @Override
+        public Size size() throws IOException {
+            return new Size(80, 24);
+        }
+
+        @Override
+        public void showCursor() throws IOException {
+        }
+
+        @Override
+        public void hideCursor() throws IOException {
+        }
+
+        @Override
+        public Position getCursorPosition() throws IOException {
+            return new Position(0, 0);
+        }
+
+        @Override
+        public void setCursorPosition(Position position) throws IOException {
+        }
+
+        @Override
+        public void enterAlternateScreen() throws IOException {
+        }
+
+        @Override
+        public void leaveAlternateScreen() throws IOException {
+        }
+
+        @Override
+        public void enableRawMode() throws IOException {
+        }
+
+        @Override
+        public void disableRawMode() throws IOException {
+        }
+
+        @Override
+        public void onResize(Runnable handler) {
+        }
+
+        @Override
+        public int read(int timeoutMs) throws IOException {
+            return -2;
+        }
+
+        @Override
+        public int peek(int timeoutMs) throws IOException {
+            return -2;
+        }
+
+        @Override
+        public void close() throws IOException {
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommandRecordOptionsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommandRecordOptionsTest.java
new file mode 100644
index 000000000000..3dfb57e50cde
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiCommandRecordOptionsTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.tui;
+
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verifies that {@code camel tui} forwards the recording options to {@link 
CamelMonitor}.
+ * <p>
+ * {@link TuiCommand} is the entry point registered by the TUI plugin, but the 
options are implemented on
+ * {@link CamelMonitor}, which it delegates to by rebuilding a command line. 
An option declared on only one of the two
+ * fails in a way no compiler catches: declared only on {@code CamelMonitor} 
it is rejected as an unknown option,
+ * declared only on {@code TuiCommand} it is accepted and then silently 
dropped. This test pins the forwarding so the
+ * documented {@code camel tui --record=demo.tape --record-size=160x44 
--record-fps=15} keeps working.
+ */
+class TuiCommandRecordOptionsTest {
+
+    @Test
+    void forwardsAllRecordingOptions() {
+        assertThat(buildArgs("--record=demo.tape", "--record-size=160x44", 
"--record-fps=15", "--record-duration=30000"))
+                .containsExactly(
+                        "--record", "demo.tape",
+                        "--record-size", "160x44",
+                        "--record-fps", "15",
+                        "--record-duration", "30000");
+    }
+
+    @Test
+    void omitsRecordingOptionsLeftAtTheirDefault() {
+        // Passing the defaults through would be harmless but noisy; more 
importantly the delegate must keep
+        // owning the default values, so they are only declared in one place.
+        
assertThat(buildArgs("--record=demo.tape")).containsExactly("--record", 
"demo.tape");
+    }
+
+    @Test
+    void everyRecordingOptionIsAcceptedByTheDelegate() {
+        // The forwarded command line is only useful if CamelMonitor 
understands it — the bug this guards against
+        // was TuiCommand accepting --record-size and CamelMonitor never 
seeing it.
+        CamelMonitor monitor = new CamelMonitor(new CamelJBangMain(), 
getClass().getClassLoader());
+        String[] args = buildArgs("--record=demo.tape", 
"--record-size=160x44", "--record-fps=15",
+                "--record-duration=30000").toArray(String[]::new);
+
+        new CommandLine(monitor).parseArgs(args);
+
+        assertThat(monitor.record).isEqualTo("demo.tape");
+        assertThat(monitor.recordSize).isEqualTo("160x44");
+        assertThat(monitor.recordFps).isEqualTo(15);
+        assertThat(monitor.recordDuration).isEqualTo(30000);
+    }
+
+    private List<String> buildArgs(String... args) {
+        TuiCommand command = new TuiCommand(new CamelJBangMain(), 
getClass().getClassLoader());
+        new CommandLine(command).parseArgs(args);
+        return command.buildArgs();
+    }
+}

Reply via email to