mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 00:51:25 -07:00
fix(core): fix PTY descriptor shell leak (#16773)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user