Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 61 additions & 20 deletions tests/ipc/runtime/backgroundBashDirect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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;
Expand Down Expand Up @@ -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,
Expand All @@ -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}"`,
Expand All @@ -444,7 +484,7 @@ describe("Foreground to Background Migration", () => {
) as Promise<ToolExecuteResult>;

// 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);
Expand Down Expand Up @@ -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,
Expand All @@ -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}"`;

Expand All @@ -534,7 +575,7 @@ describe("Foreground to Background Migration", () => {
) as Promise<ToolExecuteResult>;

// 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);
Expand Down Expand Up @@ -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,
Expand All @@ -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!(
{
Expand All @@ -679,7 +720,7 @@ describe("Foreground to Background Migration", () => {
) as Promise<ToolExecuteResult>;

// 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);
Expand Down
Loading