fix(core): fix PTY descriptor shell leak (#16773)

This commit is contained in:
Gal Zahavi
2026-01-16 09:55:29 -08:00
committed by GitHub
parent be37c26c88
commit 013a4e02ff
4 changed files with 171 additions and 5 deletions

View File

@@ -30,11 +30,20 @@ const mockIsBinary = vi.hoisted(() => vi.fn());
const mockPlatform = vi.hoisted(() => vi.fn());
const mockGetPty = vi.hoisted(() => vi.fn());
const mockSerializeTerminalToObject = vi.hoisted(() => vi.fn());
const mockResolveExecutable = vi.hoisted(() => vi.fn());
// Top-level Mocks
vi.mock('@lydell/node-pty', () => ({
spawn: mockPtySpawn,
}));
vi.mock('../utils/shell-utils.js', async (importOriginal) => {
const actual =
await importOriginal<typeof import('../utils/shell-utils.js')>();
return {
...actual,
resolveExecutable: mockResolveExecutable,
};
});
vi.mock('node:child_process', async (importOriginal) => {
const actual = await importOriginal();
return {
@@ -45,7 +54,7 @@ vi.mock('node:child_process', async (importOriginal) => {
vi.mock('../utils/textUtils.js', () => ({
isBinary: mockIsBinary,
}));
vi.mock('os', () => ({
vi.mock('node:os', () => ({
default: {
platform: mockPlatform,
constants: {
@@ -136,6 +145,7 @@ describe('ShellExecutionService', () => {
onExit: Mock;
write: Mock;
resize: Mock;
destroy: Mock;
};
let mockHeadlessTerminal: {
resize: Mock;
@@ -153,6 +163,8 @@ describe('ShellExecutionService', () => {
mockSerializeTerminalToObject.mockReturnValue([]);
mockIsBinary.mockReturnValue(false);
mockPlatform.mockReturnValue('linux');
mockResolveExecutable.mockImplementation(async (exe: string) => exe);
process.env['PATH'] = '/test/path';
mockGetPty.mockResolvedValue({
module: { spawn: mockPtySpawn },
name: 'mock-pty',
@@ -167,6 +179,7 @@ describe('ShellExecutionService', () => {
onExit: Mock;
write: Mock;
resize: Mock;
destroy: Mock;
};
mockPtyProcess.pid = 12345;
mockPtyProcess.kill = vi.fn();
@@ -174,6 +187,7 @@ describe('ShellExecutionService', () => {
mockPtyProcess.onExit = vi.fn();
mockPtyProcess.write = vi.fn();
mockPtyProcess.resize = vi.fn();
mockPtyProcess.destroy = vi.fn();
mockHeadlessTerminal = {
resize: vi.fn(),
@@ -821,6 +835,31 @@ describe('ShellExecutionService', () => {
);
});
});
describe('Resource Management', () => {
it('should destroy the PTY process and clear activePtys on exit', async () => {
await simulateExecution('ls -l', (pty) => {
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
});
expect(mockPtyProcess.destroy).toHaveBeenCalled();
expect(ShellExecutionService['activePtys'].size).toBe(0);
});
it('should destroy the PTY process even if destroy throws', async () => {
mockPtyProcess.destroy.mockImplementation(() => {
throw new Error('Destroy failed');
});
await expect(
simulateExecution('ls -l', (pty) => {
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
}),
).resolves.not.toThrow();
expect(ShellExecutionService['activePtys'].size).toBe(0);
});
});
});
describe('ShellExecutionService child_process fallback', () => {