fix(core): resolve PTY exhaustion and orphan MCP subprocess leaks (#25079)

This commit is contained in:
Spencer
2026-04-10 14:57:09 -04:00
committed by GitHub
parent a74bb603c0
commit 128e3567cb
4 changed files with 160 additions and 71 deletions
@@ -75,6 +75,7 @@ vi.mock('../utils/shell-utils.js', async (importOriginal) => {
return {
...actual,
resolveExecutable: mockResolveExecutable,
spawnAsync: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }),
};
});
vi.mock('node:child_process', async (importOriginal) => {
@@ -695,7 +696,7 @@ describe('ShellExecutionService', () => {
);
expect(sigtermCallIndex).toBe(0);
expect(sigkillCallIndex).toBe(1);
expect(sigkillCallIndex).toBeGreaterThan(0);
expect(sigtermCallIndex).toBeLessThan(sigkillCallIndex);
expect(result.signal).toBe(9);
@@ -1476,8 +1477,11 @@ describe('ShellExecutionService child_process fallback', () => {
const { result } = await simulateExecution(
'sleep 10',
(cp, abortController) => {
async (cp, abortController) => {
abortController.abort();
await new Promise(process.nextTick);
await new Promise(process.nextTick);
await new Promise(process.nextTick);
if (expectedExit.signal) {
cp.emit('exit', null, expectedExit.signal);
cp.emit('close', null, expectedExit.signal);
@@ -1497,11 +1501,14 @@ describe('ShellExecutionService child_process fallback', () => {
expectedSignal,
);
} else {
expect(mockCpSpawn).toHaveBeenCalledWith(
expectedCommand,
['/pid', String(mockChildProcess.pid), '/f', '/t'],
expect.anything(),
);
// Taskkill is spawned via spawnAsync which is mocked
const { spawnAsync } = await import('../utils/shell-utils.js');
expect(spawnAsync).toHaveBeenCalledWith(expectedCommand, [
'/pid',
String(mockChildProcess.pid),
'/f',
'/t',
]);
}
});
},
@@ -1531,6 +1538,7 @@ describe('ShellExecutionService child_process fallback', () => {
);
abortController.abort();
await vi.advanceTimersByTimeAsync(0);
// Check the first kill signal
expect(mockProcessKill).toHaveBeenCalledWith(
@@ -1733,10 +1741,12 @@ describe('ShellExecutionService execution method selection', () => {
);
// Simulate exit to allow promise to resolve
if (!mockPtyProcess.onExit.mock.calls[0]) {
const res = await handle.result;
throw new Error(`Failed early in executeWithPty: ${res.error}`);
}
mockPtyProcess.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
const result = await handle.result;
expect(mockGetPty).toHaveBeenCalled();
expect(mockPtySpawn).toHaveBeenCalled();
expect(mockCpSpawn).not.toHaveBeenCalled();
expect(result.executionMethod).toBe('mock-pty');