Files
qwen-code/packages/core/src/ide/process-utils.test.ts
Shreya Keshive 2fb14ead1f Hotfix for issue #7730 (#7739)
Co-authored-by: Taylor Mullen <ntaylormullen@google.com>
Co-authored-by: Arya Gummadi <aryagummadi@google.com>
2025-09-04 12:46:02 -07:00

197 lines
6.3 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
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();
});
describe('on Unix', () => {
it('should traverse up to find the shell and return grandparent process info', async () => {
(os.platform as Mock).mockReturnValue('linux');
// process (1000) -> shell (800) -> IDE (700)
mockedExec
.mockResolvedValueOnce({ stdout: '800 /bin/bash' }) // pid 1000 -> ppid 800 (shell)
.mockResolvedValueOnce({ stdout: '700 /usr/lib/vscode/code' }) // pid 800 -> ppid 700 (IDE)
.mockResolvedValueOnce({ stdout: '700 /usr/lib/vscode/code' }); // get command for pid 700
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' }) // pid 1000 -> ppid 800 (shell)
.mockRejectedValueOnce(new Error('ps failed')) // lookup for ppid of 800 fails
.mockResolvedValueOnce({ stdout: '800 /bin/bash' }); // get command for pid 800
const result = await getIdeProcessInfo();
expect(result).toEqual({ pid: 800, command: '/bin/bash' });
});
});
describe('on Windows', () => {
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:
'{"Name":"node.exe","ParentProcessId":900,"CommandLine":"node.exe"}',
},
],
[
900,
{
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"}',
},
],
]);
mockedExec.mockImplementation((command: string) => {
const pidMatch = command.match(/ProcessId=(\d+)/);
if (pidMatch) {
const pid = parseInt(pidMatch[1], 10);
return Promise.resolve(processInfoMap.get(pid));
}
return Promise.reject(new Error('Invalid command for mock'));
});
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' });
});
});
});