pi-mono/packages/coding-agent/test/bash-close-hang-windows.test.ts
Mario Zechner 5a4e22ea44 fix(coding-agent): remove process-cwd tool singletons and use tool-name allowlists
- switch SDK/CLI tool selection to name-based allowlists
- apply allowlists across built-in, extension, and SDK tools
- remove ambient process.cwd defaults from core tooling and resource helpers
- update tests, examples, and TUI callers for explicit cwd plumbing
- add regression coverage for extension tool filtering

closes #3452
closes #2835
2026-04-20 22:05:28 +02:00

126 lines
3.7 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { executeBashWithOperations } from "../src/core/bash-executor.js";
import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
function toBashSingleQuotedArg(value: string): string {
return `'${value.replace(/\\/g, "/").replace(/'/g, `'"'"'`)}'`;
}
function createInheritedStdioCommand(pidFile: string): string {
const pidFileArg = toBashSingleQuotedArg(pidFile);
return (
'node -e "' +
"const fs=require('fs');" +
"const {spawn}=require('child_process');" +
"const child=spawn(process.execPath,['-e','setTimeout(()=>{},60000)'],{stdio:'inherit',detached:true});" +
"fs.writeFileSync(process.argv[1], String(child.pid));" +
"child.unref();" +
"console.log('child-exiting');" +
'" ' +
pidFileArg
);
}
function cleanupDetachedChild(pidFile: string): void {
if (!existsSync(pidFile)) {
return;
}
const pid = Number.parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
if (Number.isFinite(pid) && pid > 0) {
try {
execFileSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
} catch {
// Process may have already exited.
}
}
}
async function withTimeout<T>(promise: Promise<T>, ms: number, onTimeout: () => void): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutId = setTimeout(() => {
onTimeout();
reject(new Error(`Timed out after ${ms}ms`));
}, ms);
promise.then(
(value) => {
clearTimeout(timeoutId);
resolve(value);
},
(error: unknown) => {
clearTimeout(timeoutId);
reject(error);
},
);
});
}
function getTextOutput(result: { content?: Array<{ type: string; text?: string }> }): string {
return (
result.content
?.filter((block) => block.type === "text")
.map((block) => block.text ?? "")
.join("\n") ?? ""
);
}
describe.skipIf(process.platform !== "win32")("Windows child-process close handling", () => {
let testDir: string;
beforeEach(() => {
testDir = join(tmpdir(), `coding-agent-bash-close-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
mkdirSync(testDir, { recursive: true });
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
it("executeBash resolves after the shell exits even if inherited stdio handles stay open", async () => {
const pidFile = join(testDir, "executor-grandchild.pid");
const command = createInheritedStdioCommand(pidFile);
const controller = new AbortController();
try {
const result = await withTimeout(
executeBashWithOperations(command, process.cwd(), createLocalBashOperations(), {
signal: controller.signal,
}),
3000,
() => {
controller.abort();
},
);
expect(result.output).toContain("child-exiting");
expect(result.exitCode).toBe(0);
expect(result.cancelled).toBe(false);
} finally {
controller.abort();
cleanupDetachedChild(pidFile);
}
});
it("bash tool resolves after the shell exits even if inherited stdio handles stay open", async () => {
const pidFile = join(testDir, "tool-grandchild.pid");
const command = createInheritedStdioCommand(pidFile);
const controller = new AbortController();
const bashTool = createBashTool(testDir);
try {
const result = await withTimeout(bashTool.execute("test-call", { command }, controller.signal), 3000, () => {
controller.abort();
});
expect(getTextOutput(result)).toContain("child-exiting");
} finally {
controller.abort();
cleanupDetachedChild(pidFile);
}
});
});