From 777d85488432d0dec6cbf637cbe3a3bd8855c3ff Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 15 Jun 2026 10:28:28 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20tests:=20stabilize=20foreground?= =?UTF-8?q?=20bash=20migration=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wait for actual foreground bash output before sending migration tests to the background, instead of relying on CI-dependent fixed sleeps. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `$20.62`_ --- .../ipc/runtime/backgroundBashDirect.test.ts | 81 ++++++++++++++----- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/tests/ipc/runtime/backgroundBashDirect.test.ts b/tests/ipc/runtime/backgroundBashDirect.test.ts index db538858c6..1c2d66cbfd 100644 --- a/tests/ipc/runtime/backgroundBashDirect.test.ts +++ b/tests/ipc/runtime/backgroundBashDirect.test.ts @@ -41,7 +41,46 @@ function getBackgroundProcessManager(env: TestEnvironment): BackgroundProcessMan } // Foreground bash startup can be slower on Windows CI (Git Bash init + IO flush). -const FOREGROUND_MIGRATION_READY_MS = process.platform === "win32" ? 900 : 300; +const FOREGROUND_OUTPUT_READY_TIMEOUT_MS = process.platform === "win32" ? 10_000 : 5_000; + +interface BashOutputCollector { + readonly chunks: string[]; + emitChatEvent: (event: { type: string; toolCallId?: string; text?: string }) => void; +} + +function createBashOutputCollector(toolCallId: string): BashOutputCollector { + const chunks: string[] = []; + return { + chunks, + emitChatEvent: (event) => { + if (event.type === "bash-output" && event.toolCallId === toolCallId && event.text) { + // Foreground migration tests must wait for real stdout readiness rather than + // assuming Git Bash has flushed within a fixed CI-dependent delay. + chunks.push(event.text); + } + }, + }; +} + +async function waitForBashOutput( + chunks: readonly string[], + expectedText: string, + timeoutMs: number +): Promise { + if (expectedText.length === 0) { + throw new Error("waitForBashOutput requires non-empty expected text"); + } + + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (chunks.some((chunk) => chunk.includes(expectedText))) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 25)); + } + + throw new Error(`Timed out waiting for bash output: ${expectedText}`); +} interface ToolExecuteResult { success: boolean; @@ -412,6 +451,11 @@ describe("Foreground to Background Migration", () => { const manager = getBackgroundProcessManager(env); const runtime = new LocalRuntime(workspacePath); + const testId = `fg_to_bg_${Date.now()}`; + const marker1 = `BEFORE_BG_${testId}`; + const marker2 = `AFTER_BG_${testId}`; + const toolCallId = `tool_${testId}`; + const outputCollector = createBashOutputCollector(toolCallId); const toolConfig = { cwd: workspacePath, @@ -421,18 +465,14 @@ describe("Foreground to Background Migration", () => { runtimeTempDir: os.tmpdir(), backgroundProcessManager: manager, workspaceId, + emitChatEvent: outputCollector.emitChatEvent, }; - const testId = `fg_to_bg_${Date.now()}`; - const marker1 = `BEFORE_BG_${testId}`; - const marker2 = `AFTER_BG_${testId}`; - // Create tools for "message 1" const bash1 = createBashTool(toolConfig); // Start foreground bash that runs for ~3 seconds // Script: output marker1, sleep, output marker2 - const toolCallId = `tool_${testId}`; const bashPromise = bash1.execute!( { script: `echo "${marker1}"; sleep 2; echo "${marker2}"`, @@ -444,7 +484,7 @@ describe("Foreground to Background Migration", () => { ) as Promise; // Wait for foreground process to register and output first marker - await new Promise((resolve) => setTimeout(resolve, FOREGROUND_MIGRATION_READY_MS)); + await waitForBashOutput(outputCollector.chunks, marker1, FOREGROUND_OUTPUT_READY_TIMEOUT_MS); // Verify foreground process is registered const fgToolCallIds = manager.getForegroundToolCallIds(workspaceId); @@ -502,6 +542,11 @@ describe("Foreground to Background Migration", () => { const manager = getBackgroundProcessManager(env); const runtime = new LocalRuntime(workspacePath); + const testId = `preserve_output_${Date.now()}`; + const marker1 = `EARLY_${testId}`; + const marker2 = `LATE_${testId}`; + const toolCallId = `tool_${testId}`; + const outputCollector = createBashOutputCollector(toolCallId); const toolConfig = { cwd: workspacePath, @@ -511,15 +556,11 @@ describe("Foreground to Background Migration", () => { runtimeTempDir: os.tmpdir(), backgroundProcessManager: manager, workspaceId, + emitChatEvent: outputCollector.emitChatEvent, }; - const testId = `preserve_output_${Date.now()}`; - const marker1 = `EARLY_${testId}`; - const marker2 = `LATE_${testId}`; - const bash1 = createBashTool(toolConfig); - const toolCallId = `tool_${testId}`; // Script outputs marker1, sleeps, then outputs marker2 const script = `echo "${marker1}"; sleep 2; echo "${marker2}"`; @@ -534,7 +575,7 @@ describe("Foreground to Background Migration", () => { ) as Promise; // Wait for marker1 to output - await new Promise((resolve) => setTimeout(resolve, FOREGROUND_MIGRATION_READY_MS)); + await waitForBashOutput(outputCollector.chunks, marker1, FOREGROUND_OUTPUT_READY_TIMEOUT_MS); // Send to background mid-execution manager.sendToBackground(toolCallId); @@ -645,6 +686,11 @@ describe("Foreground to Background Migration", () => { const manager = getBackgroundProcessManager(env); const runtime = new LocalRuntime(workspacePath); + const testId = `abort_after_bg_${Date.now()}`; + const marker1 = `BEFORE_${testId}`; + const marker2 = `AFTER_${testId}`; + const toolCallId = `tool_${testId}`; + const outputCollector = createBashOutputCollector(toolCallId); const toolConfig = { cwd: workspacePath, @@ -654,19 +700,14 @@ describe("Foreground to Background Migration", () => { runtimeTempDir: os.tmpdir(), backgroundProcessManager: manager, workspaceId, + emitChatEvent: outputCollector.emitChatEvent, }; - const testId = `abort_after_bg_${Date.now()}`; - const marker1 = `BEFORE_${testId}`; - const marker2 = `AFTER_${testId}`; - // Create an AbortController to simulate stream abort const abortController = new AbortController(); const bash = createBashTool(toolConfig); - const toolCallId = `tool_${testId}`; - // Start a foreground bash with the abort signal const bashPromise = bash.execute!( { @@ -679,7 +720,7 @@ describe("Foreground to Background Migration", () => { ) as Promise; // Wait for first marker - await new Promise((resolve) => setTimeout(resolve, FOREGROUND_MIGRATION_READY_MS)); + await waitForBashOutput(outputCollector.chunks, marker1, FOREGROUND_OUTPUT_READY_TIMEOUT_MS); // Send to background manager.sendToBackground(toolCallId);