diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 86570e0a..9a596299 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -83,7 +83,7 @@ vi.mock('@google/gemini-cli-core', async () => { return { ...actualServer, IdeClient: { - getInstance: vi.fn().mockReturnValue({ + getInstance: vi.fn().mockResolvedValue({ getConnectionStatus: vi.fn(), initialize: vi.fn(), shutdown: vi.fn(), diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index d5647bee..0b1c2023 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -100,6 +100,16 @@ vi.mock('../services/gitService.js', () => { return { GitService: GitServiceMock }; }); +vi.mock('../ide/ide-client.js', () => ({ + IdeClient: { + getInstance: vi.fn().mockResolvedValue({ + getConnectionStatus: vi.fn(), + initialize: vi.fn(), + shutdown: vi.fn(), + }), + }, +})); + describe('Server Config (config.ts)', () => { const MODEL = 'gemini-pro'; const SANDBOX: SandboxConfig = { diff --git a/packages/core/src/ide/process-utils.test.ts b/packages/core/src/ide/process-utils.test.ts index 9ac56424..e6c68f14 100644 --- a/packages/core/src/ide/process-utils.test.ts +++ b/packages/core/src/ide/process-utils.test.ts @@ -66,13 +66,34 @@ describe('getIdeProcessInfo', () => { it('should traverse up and find the great-grandchild of the root process', async () => { (os.platform as Mock).mockReturnValue('win32'); const processInfoMap = new Map([ - [1000, { stdout: 'ParentProcessId=900\r\nCommandLine=node.exe\r\n' }], + [ + 1000, + { + stdout: + '{"Name":"node.exe","ParentProcessId":900,"CommandLine":"node.exe"}', + }, + ], [ 900, - { stdout: 'ParentProcessId=800\r\nCommandLine=powershell.exe\r\n' }, + { + stdout: + '{"Name":"powershell.exe","ParentProcessId":800,"CommandLine":"powershell.exe"}', + }, + ], + [ + 800, + { + stdout: + '{"Name":"code.exe","ParentProcessId":700,"CommandLine":"code.exe"}', + }, + ], + [ + 700, + { + stdout: + '{"Name":"wininit.exe","ParentProcessId":0,"CommandLine":"wininit.exe"}', + }, ], - [800, { stdout: 'ParentProcessId=700\r\nCommandLine=code.exe\r\n' }], - [700, { stdout: 'ParentProcessId=0\r\nCommandLine=wininit.exe\r\n' }], ]); mockedExec.mockImplementation((command: string) => { const pidMatch = command.match(/ProcessId=(\d+)/); @@ -86,5 +107,90 @@ describe('getIdeProcessInfo', () => { const result = await getIdeProcessInfo(); expect(result).toEqual({ pid: 900, command: 'powershell.exe' }); }); + + it('should handle non-existent process gracefully', async () => { + (os.platform as Mock).mockReturnValue('win32'); + mockedExec + .mockResolvedValueOnce({ stdout: '' }) // Non-existent PID returns empty due to -ErrorAction SilentlyContinue + .mockResolvedValueOnce({ + stdout: + '{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}', + }); // Fallback call + + const result = await getIdeProcessInfo(); + expect(result).toEqual({ pid: 1000, command: 'fallback.exe' }); + }); + + it('should handle malformed JSON output gracefully', async () => { + (os.platform as Mock).mockReturnValue('win32'); + mockedExec + .mockResolvedValueOnce({ stdout: '{"invalid":json}' }) // Malformed JSON + .mockResolvedValueOnce({ + stdout: + '{"Name":"fallback.exe","ParentProcessId":0,"CommandLine":"fallback.exe"}', + }); // Fallback call + + const result = await getIdeProcessInfo(); + expect(result).toEqual({ pid: 1000, command: 'fallback.exe' }); + }); + + it('should handle PowerShell errors without crashing the process chain', async () => { + (os.platform as Mock).mockReturnValue('win32'); + const processInfoMap = new Map([ + [1000, { stdout: '' }], // First process doesn't exist (empty due to -ErrorAction) + [ + 1001, + { + stdout: + '{"Name":"parent.exe","ParentProcessId":800,"CommandLine":"parent.exe"}', + }, + ], + [ + 800, + { + stdout: + '{"Name":"ide.exe","ParentProcessId":0,"CommandLine":"ide.exe"}', + }, + ], + ]); + + // Mock the process.pid to test traversal with missing processes + Object.defineProperty(process, 'pid', { + value: 1001, + configurable: true, + }); + + mockedExec.mockImplementation((command: string) => { + const pidMatch = command.match(/ProcessId=(\d+)/); + if (pidMatch) { + const pid = parseInt(pidMatch[1], 10); + return Promise.resolve(processInfoMap.get(pid) || { stdout: '' }); + } + return Promise.reject(new Error('Invalid command for mock')); + }); + + const result = await getIdeProcessInfo(); + // Should return the current process command since traversal continues despite missing processes + expect(result).toEqual({ pid: 1001, command: 'parent.exe' }); + + // Reset process.pid + Object.defineProperty(process, 'pid', { + value: 1000, + configurable: true, + }); + }); + + it('should handle partial JSON data with defaults', async () => { + (os.platform as Mock).mockReturnValue('win32'); + mockedExec + .mockResolvedValueOnce({ stdout: '{"Name":"partial.exe"}' }) // Missing ParentProcessId, defaults to 0 + .mockResolvedValueOnce({ + stdout: + '{"Name":"root.exe","ParentProcessId":0,"CommandLine":"root.exe"}', + }); // Get grandparent info + + const result = await getIdeProcessInfo(); + expect(result).toEqual({ pid: 1000, command: 'root.exe' }); + }); }); }); diff --git a/packages/core/src/ide/process-utils.ts b/packages/core/src/ide/process-utils.ts index 17d1d520..451a3af3 100644 --- a/packages/core/src/ide/process-utils.ts +++ b/packages/core/src/ide/process-utils.ts @@ -26,15 +26,24 @@ async function getProcessInfo(pid: number): Promise<{ }> { const platform = os.platform(); if (platform === 'win32') { - const command = `wmic process where "ProcessId=${pid}" get Name,ParentProcessId,CommandLine /value`; - const { stdout } = await execAsync(command); - const nameMatch = stdout.match(/Name=([^\n]*)/); - const processName = nameMatch ? nameMatch[1].trim() : ''; - const ppidMatch = stdout.match(/ParentProcessId=(\d+)/); - const parentPid = ppidMatch ? parseInt(ppidMatch[1], 10) : 0; - const commandLineMatch = stdout.match(/CommandLine=([^\n]*)/); - const commandLine = commandLineMatch ? commandLineMatch[1].trim() : ''; - return { parentPid, name: processName, command: commandLine }; + const powershellCommand = [ + '$p = Get-CimInstance Win32_Process', + `-Filter 'ProcessId=${pid}'`, + '-ErrorAction SilentlyContinue;', + 'if ($p) {', + '@{Name=$p.Name;ParentProcessId=$p.ParentProcessId;CommandLine=$p.CommandLine}', + '| ConvertTo-Json', + '}', + ].join(' '); + const { stdout } = await execAsync(`powershell "${powershellCommand}"`); + const output = stdout.trim(); + if (!output) return { parentPid: 0, name: '', command: '' }; + const { + Name = '', + ParentProcessId = 0, + CommandLine = '', + } = JSON.parse(output); + return { parentPid: ParentProcessId, name: Name, command: CommandLine }; } else { const command = `ps -o ppid=,command= -p ${pid}`; const { stdout } = await execAsync(command);