This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26405-26406-flaky-tui-tests
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 5c1be2bc8b510bc71b2c92b4d24fc6eb929a60f9
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 17:55:21 2026 +0100

    CAMEL-26405, CAMEL-26406: camel-jbang - fix flaky LaunchManagerOutcomeTest 
and TuiShellHistorySupportTest
    
    LaunchManagerInfraTest stopped every sleep and sh child of the test JVM,
    so it killed the processes of tests running in parallel, such as the
    sleep of LaunchManagerOutcomeTest. launchInfra now returns the process
    it started, and the test stops only its own.
    
    TuiShellHistorySupportTest moved the home directory of the CLI without
    being isolated: other tests wrote into its temp dir while it was
    deleted. It is now @Isolated, and the test of AiPanelTest that moves the
    home directory is in its own isolated class.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/tui/LaunchManager.java |  9 ++-
 .../commands/tui/AiPanelModelPersistenceTest.java  | 90 ++++++++++++++++++++++
 .../dsl/jbang/core/commands/tui/AiPanelTest.java   | 24 ------
 .../core/commands/tui/LaunchManagerInfraTest.java  | 33 ++++----
 .../commands/tui/TuiShellHistorySupportTest.java   |  3 +
 5 files changed, 116 insertions(+), 43 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
index b00c5b87ebe9..5ef8d4c709e4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java
@@ -270,8 +270,12 @@ class LaunchManager {
         launchInfra(alias, cmd);
     }
 
-    /** Runs the command that starts the given infra service, and watches it 
until the service is up. */
-    void launchInfra(String alias, List<String> cmd) throws IOException {
+    /**
+     * Runs the command that starts the given infra service, and watches it 
until the service is up.
+     *
+     * @return the started process
+     */
+    Process launchInfra(String alias, List<String> cmd) throws IOException {
         Path outputFile = createSecureTempFile("camel-infra-", ".log");
         outputFile.toFile().deleteOnExit();
         ProcessBuilder pb = new ProcessBuilder(cmd);
@@ -281,6 +285,7 @@ class LaunchManager {
         PendingLaunch pl = new PendingLaunch(alias, process, outputFile, 
MonotonicClock.millis());
         pl.infraAlias = serviceOf(alias);
         pendingLaunches.add(pl);
+        return process;
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelModelPersistenceTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelModelPersistenceTest.java
new file mode 100644
index 000000000000..749d8d3124fa
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelModelPersistenceTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.nio.file.Path;
+import java.util.List;
+
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
+import org.apache.camel.dsl.jbang.core.commands.LlmClient;
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The model chosen with /model is saved in the TUI settings, so it survives a 
restart.
+ */
+// it moves the home directory of the CLI, where other tests would look for 
theirs
+@Isolated
+class AiPanelModelPersistenceTest {
+
+    @TempDir
+    Path home;
+
+    private Path previousHome;
+
+    @BeforeEach
+    void setup() {
+        previousHome = CommandLineHelper.getHomeDir();
+        CommandLineHelper.useHomeDir(home.toString());
+    }
+
+    @AfterEach
+    void restore() {
+        CommandLineHelper.useHomeDir(previousHome.toString());
+    }
+
+    @Test
+    void modelCommandPersistsSelectionAcrossRestarts() {
+        AiPanel panel = new AiPanel();
+        panel.setClientForTesting(new ModelListingLlmClient(List.of("model-a", 
"model-b")));
+        panel.open();
+
+        for (char ch : "/model model-b".toCharArray()) {
+            panel.handleKeyEvent(KeyEvent.ofChar(ch));
+        }
+        panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+
+        assertThat(TuiSettings.load().getAiModel())
+                .as("the /model selection must be persisted so it survives a 
TUI restart")
+                .isEqualTo("model-b");
+        assertThat(panel.conversationForTesting())
+                .anyMatch(entry -> entry.text().contains("Switched model to 
model-b"));
+    }
+
+    private static final class ModelListingLlmClient extends LlmClient {
+
+        private final List<String> models;
+
+        ModelListingLlmClient(List<String> models) {
+            this.models = models;
+            withModel("test-model");
+        }
+
+        @Override
+        public List<String> listModels() {
+            return models;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
index 64afd11ddc2c..170dc5ae717a 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
@@ -16,7 +16,6 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
-import java.nio.file.Path;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
@@ -32,10 +31,8 @@ import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.tui.event.KeyModifiers;
 import org.apache.camel.dsl.jbang.core.commands.LlmClient;
-import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.util.json.JsonObject;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
 
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -359,27 +356,6 @@ class AiPanelTest {
         assertTrue(listThread.get().contains("model-list"));
     }
 
-    @Test
-    void modelCommandPersistsSelectionAcrossRestarts(@TempDir Path tempDir) {
-        String originalHome = CommandLineHelper.getHomeDir().toString();
-        CommandLineHelper.useHomeDir(tempDir.toString());
-        try {
-            AiPanel panel = new AiPanel();
-            panel.setClientForTesting(new 
ModelListingLlmClient(List.of("model-a", "model-b")));
-            panel.open();
-
-            type(panel, "/model model-b");
-            panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, 
KeyModifiers.NONE));
-
-            assertEquals("model-b", TuiSettings.load().getAiModel(),
-                    "the /model selection must be persisted so it survives a 
TUI restart");
-            assertTrue(panel.conversationForTesting().stream()
-                    .anyMatch(entry -> entry.text().contains("Switched model 
to model-b")));
-        } finally {
-            CommandLineHelper.useHomeDir(originalHome);
-        }
-    }
-
     @Test
     void unknownCommandRendersHelpHint() {
         AiPanel panel = new AiPanel();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
index 7bd8a8bcc9ad..21589f75a28c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java
@@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -35,6 +36,16 @@ class LaunchManagerInfraTest {
 
     private final List<InfraInfo> running = new ArrayList<>();
     private final LaunchManager lm = new LaunchManager(() -> running);
+    // the processes this test started: only these are stopped, as other tests 
run processes of their own
+    private final List<Process> processes = new ArrayList<>();
+
+    @AfterEach
+    void stopProcesses() {
+        for (Process p : processes) {
+            p.descendants().forEach(ProcessHandle::destroy);
+            p.destroy();
+        }
+    }
 
     @Test
     void theServiceOfAnEntryWithItsImplementation() {
@@ -59,7 +70,7 @@ class LaunchManagerInfraTest {
         lm.setNotificationCallback((msg, error) -> notices.add(msg));
         AtomicBoolean launched = new AtomicBoolean();
         // a first start pulls the image: the infra takes a while
-        lm.launchInfra("kafka", List.of("sleep", "30"));
+        processes.add(lm.launchInfra("kafka", List.of("sleep", "30")));
         lm.deferUntilInfra(List.of("kafka"), "kafka-orders", () -> 
launched.set(true));
 
         lm.tick(MonotonicClock.millis());
@@ -70,16 +81,13 @@ class LaunchManagerInfraTest {
         lm.tick(MonotonicClock.millis());
         assertThat(launched).isTrue();
         assertThat(notices).contains("Started: kafka");
-        ProcessHandle.current().children()
-                .filter(p -> p.info().command().map(c -> 
c.endsWith("sleep")).orElse(false))
-                .forEach(ProcessHandle::destroy);
     }
 
     @Test
     void aLaunchThatNeedsAServiceWithItsImplementationRunsOnceTheServiceIsUp() 
throws Exception {
         AtomicBoolean launched = new AtomicBoolean();
         // camel infra run aws sqs runs as the aws service
-        lm.launchInfra("aws sqs", List.of("sleep", "30"));
+        processes.add(lm.launchInfra("aws sqs", List.of("sleep", "30")));
         lm.deferUntilInfra(List.of("aws sqs"), "aws-sqs", () -> 
launched.set(true));
 
         lm.tick(MonotonicClock.millis());
@@ -88,9 +96,6 @@ class LaunchManagerInfraTest {
         running.add(infra("aws"));
         lm.tick(MonotonicClock.millis());
         assertThat(launched).isTrue();
-        ProcessHandle.current().children()
-                .filter(p -> p.info().command().map(c -> 
c.endsWith("sleep")).orElse(false))
-                .forEach(ProcessHandle::destroy);
     }
 
     @Test
@@ -101,7 +106,7 @@ class LaunchManagerInfraTest {
         lm.setFailureLogCallback((name, log) -> failures.add(name));
         AtomicBoolean launched = new AtomicBoolean();
         // the container cannot bind its port: camel infra run ends with an 
error
-        lm.launchInfra("mosquitto", List.of("sh", "-c", "echo port is already 
allocated; exit 1"));
+        processes.add(lm.launchInfra("mosquitto", List.of("sh", "-c", "echo 
port is already allocated; exit 1")));
         lm.deferUntilInfra(List.of("mosquitto"), "mqtt", () -> 
launched.set(true));
         await().atMost(10, TimeUnit.SECONDS).pollInterval(50, 
TimeUnit.MILLISECONDS).pollInSameThread().until(() -> {
             lm.tick(MonotonicClock.millis());
@@ -137,8 +142,8 @@ class LaunchManagerInfraTest {
     void theProgressOfAPullIsShownWhileTheInfraStarts() throws Exception {
         List<String> notices = new ArrayList<>();
         lm.setNotificationCallback((msg, error) -> notices.add(msg));
-        lm.launchInfra("ollama", List.of("sh", "-c",
-                "echo 'Pulling image layers:  3 pending,  2 downloaded,  0 
extracted, (1.2 GB/3.4 GB)'; sleep 30"));
+        processes.add(lm.launchInfra("ollama", List.of("sh", "-c",
+                "echo 'Pulling image layers:  3 pending,  2 downloaded,  0 
extracted, (1.2 GB/3.4 GB)'; sleep 30")));
         await().atMost(10, TimeUnit.SECONDS).pollInterval(50, 
TimeUnit.MILLISECONDS).pollInSameThread().until(() -> {
             lm.tick(MonotonicClock.millis());
             return !notices.isEmpty();
@@ -148,12 +153,6 @@ class LaunchManagerInfraTest {
         // the same progress is not shown again
         lm.tick(MonotonicClock.millis());
         assertThat(notices).hasSize(1);
-        ProcessHandle.current().children()
-                .filter(p -> p.info().command().map(c -> 
c.endsWith("sh")).orElse(false))
-                .forEach(ph -> {
-                    ph.descendants().forEach(ProcessHandle::destroy);
-                    ph.destroy();
-                });
     }
 
     private static InfraInfo infra(String alias) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java
index 4deac3c444b0..30f7d6d3d772 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java
@@ -28,9 +28,12 @@ import org.jline.terminal.TerminalBuilder;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.Isolated;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
+// it moves the home directory of the CLI: other tests would write their files 
into its temp dir while it is deleted
+@Isolated
 class TuiShellHistorySupportTest {
 
     private String originalHome;

Reply via email to