| |
| |
| |
| |
| |
|
|
| import { |
| describe, |
| it, |
| expect, |
| vi, |
| afterEach, |
| beforeEach, |
| type Mock, |
| } from 'vitest'; |
| import { getIdeProcessInfo } from './process-utils.js'; |
| import os from 'node:os'; |
|
|
| const mockedExec = vi.hoisted(() => vi.fn()); |
| vi.mock('node:util', () => ({ |
| promisify: vi.fn().mockReturnValue(mockedExec), |
| })); |
| vi.mock('node:os', () => ({ |
| default: { |
| platform: vi.fn(), |
| }, |
| })); |
|
|
| describe('getIdeProcessInfo', () => { |
| beforeEach(() => { |
| Object.defineProperty(process, 'pid', { value: 1000, configurable: true }); |
| mockedExec.mockReset(); |
| }); |
|
|
| afterEach(() => { |
| vi.restoreAllMocks(); |
| vi.unstubAllEnvs(); |
| }); |
|
|
| describe('GEMINI_CLI_IDE_PID override', () => { |
| it('should use GEMINI_CLI_IDE_PID and fetch command on Unix', async () => { |
| (os.platform as Mock).mockReturnValue('linux'); |
| vi.stubEnv('GEMINI_CLI_IDE_PID', '12345'); |
| mockedExec.mockResolvedValueOnce({ stdout: '0 my-ide-command' }); |
|
|
| const result = await getIdeProcessInfo(); |
|
|
| expect(result).toEqual({ pid: 12345, command: 'my-ide-command' }); |
| expect(mockedExec).toHaveBeenCalledWith( |
| expect.stringContaining('ps -o ppid=,command= -p 12345'), |
| { timeout: 3000 }, |
| ); |
| }); |
|
|
| it('should use GEMINI_CLI_IDE_PID and fetch command on Windows', async () => { |
| (os.platform as Mock).mockReturnValue('win32'); |
| vi.stubEnv('GEMINI_CLI_IDE_PID', '54321'); |
| const processes = [ |
| { |
| ProcessId: 54321, |
| ParentProcessId: 0, |
| Name: 'Code.exe', |
| CommandLine: 'C:\\Program Files\\VSCode\\Code.exe', |
| }, |
| ]; |
| mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) }); |
|
|
| const result = await getIdeProcessInfo(); |
|
|
| expect(result).toEqual({ |
| pid: 54321, |
| command: 'C:\\Program Files\\VSCode\\Code.exe', |
| }); |
| expect(mockedExec).toHaveBeenCalledWith( |
| expect.stringContaining( |
| 'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine', |
| ), |
| expect.anything(), |
| ); |
| }); |
| }); |
|
|
| describe('on Unix', () => { |
| it('should traverse up to find the shell and return grandparent process info', async () => { |
| (os.platform as Mock).mockReturnValue('linux'); |
| |
| mockedExec |
| .mockResolvedValueOnce({ stdout: '800 /bin/bash' }) |
| .mockResolvedValueOnce({ stdout: '700 /usr/lib/vscode/code' }) |
| .mockResolvedValueOnce({ stdout: '700 /usr/lib/vscode/code' }); |
|
|
| const result = await getIdeProcessInfo(); |
|
|
| expect(result).toEqual({ pid: 700, command: '/usr/lib/vscode/code' }); |
| }); |
|
|
| it('should return parent process info if grandparent lookup fails', async () => { |
| (os.platform as Mock).mockReturnValue('linux'); |
| mockedExec |
| .mockResolvedValueOnce({ stdout: '800 /bin/bash' }) |
| .mockRejectedValueOnce(new Error('ps failed')) |
| .mockResolvedValueOnce({ stdout: '800 /bin/bash' }); |
|
|
| const result = await getIdeProcessInfo(); |
| expect(result).toEqual({ pid: 800, command: '/bin/bash' }); |
| }); |
|
|
| it('should handle timeout when ps command hangs or rejects during getProcessInfo', async () => { |
| (os.platform as Mock).mockReturnValue('linux'); |
| const timeoutError = new Error('exec timeout'); |
| Object.assign(timeoutError, { |
| killed: true, |
| code: undefined, |
| signal: 'SIGTERM', |
| }); |
| mockedExec.mockRejectedValue(timeoutError); |
|
|
| const result = await getIdeProcessInfo(); |
|
|
| expect(result).toEqual({ pid: 1000, command: '' }); |
| }); |
| }); |
|
|
| describe('on Windows', () => { |
| it('should traverse up and find the great-grandchild of the root process', async () => { |
| (os.platform as Mock).mockReturnValue('win32'); |
| |
| |
| |
| const processes = [ |
| { |
| ProcessId: 1000, |
| ParentProcessId: 900, |
| Name: 'node.exe', |
| CommandLine: 'node.exe', |
| }, |
| { |
| ProcessId: 900, |
| ParentProcessId: 800, |
| Name: 'powershell.exe', |
| CommandLine: 'powershell.exe', |
| }, |
| { |
| ProcessId: 800, |
| ParentProcessId: 700, |
| Name: 'code.exe', |
| CommandLine: 'code.exe', |
| }, |
| { |
| ProcessId: 700, |
| ParentProcessId: 0, |
| Name: 'wininit.exe', |
| CommandLine: 'wininit.exe', |
| }, |
| ]; |
| mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) }); |
|
|
| const result = await getIdeProcessInfo(); |
| expect(result).toEqual({ pid: 900, command: 'powershell.exe' }); |
| expect(mockedExec).toHaveBeenCalledWith( |
| expect.stringContaining('Get-CimInstance Win32_Process'), |
| expect.anything(), |
| ); |
| }); |
|
|
| it('should handle short process chains', async () => { |
| (os.platform as Mock).mockReturnValue('win32'); |
| |
| const processes = [ |
| { |
| ProcessId: 1000, |
| ParentProcessId: 0, |
| Name: 'node.exe', |
| CommandLine: 'node.exe', |
| }, |
| ]; |
| mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) }); |
|
|
| const result = await getIdeProcessInfo(); |
| expect(result).toEqual({ pid: 1000, command: 'node.exe' }); |
| }); |
|
|
| it('should handle PowerShell failure gracefully', async () => { |
| (os.platform as Mock).mockReturnValue('win32'); |
| mockedExec.mockRejectedValueOnce(new Error('PowerShell failed')); |
| |
| mockedExec.mockResolvedValueOnce({ stdout: '' }); |
|
|
| const result = await getIdeProcessInfo(); |
| expect(result).toEqual({ pid: 1000, command: '' }); |
| }); |
|
|
| it('should handle malformed JSON output gracefully', async () => { |
| (os.platform as Mock).mockReturnValue('win32'); |
| mockedExec.mockResolvedValueOnce({ stdout: '{"invalid":json}' }); |
| |
| mockedExec.mockResolvedValueOnce({ stdout: '' }); |
|
|
| const result = await getIdeProcessInfo(); |
| expect(result).toEqual({ pid: 1000, command: '' }); |
| }); |
|
|
| it('should handle single process output from ConvertTo-Json', async () => { |
| (os.platform as Mock).mockReturnValue('win32'); |
| const process = { |
| ProcessId: 1000, |
| ParentProcessId: 0, |
| Name: 'node.exe', |
| CommandLine: 'node.exe', |
| }; |
| mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(process) }); |
|
|
| const result = await getIdeProcessInfo(); |
| expect(result).toEqual({ pid: 1000, command: 'node.exe' }); |
| }); |
|
|
| it('should handle missing process in map during traversal', async () => { |
| (os.platform as Mock).mockReturnValue('win32'); |
| |
| const processes = [ |
| { |
| ProcessId: 1000, |
| ParentProcessId: 900, |
| Name: 'node.exe', |
| CommandLine: 'node.exe', |
| }, |
| { |
| ProcessId: 900, |
| ParentProcessId: 800, |
| Name: 'parent.exe', |
| CommandLine: 'parent.exe', |
| }, |
| ]; |
| mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) }); |
|
|
| const result = await getIdeProcessInfo(); |
| |
| expect(result).toEqual({ pid: 900, command: 'parent.exe' }); |
| }); |
| }); |
| }); |
|
|