Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLISessionTracker.spec.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { beforeEach, describe, expect, it, vi } from 'vitest';67type MockTerminal = { processId: Promise<number | undefined>; name: string };89const { mockTerminals, terminalCloseListeners, mockExecFile, mockIsWindows } = vi.hoisted(() => ({10mockTerminals: { value: [] as Array<MockTerminal> },11terminalCloseListeners: [] as Array<(terminal: MockTerminal) => void>,12mockExecFile: vi.fn(),13mockIsWindows: { value: false },14}));1516vi.mock('vscode', async (importOriginal) => {17const actual = await importOriginal() as Record<string, unknown>;18return {19...actual,20window: {21get terminals() { return mockTerminals.value; },22onDidCloseTerminal(listener: (terminal: MockTerminal) => void) {23terminalCloseListeners.push(listener);24return { dispose: () => { const idx = terminalCloseListeners.indexOf(listener); if (idx >= 0) { terminalCloseListeners.splice(idx, 1); } } };25},26},27};28});2930vi.mock('child_process', () => ({31execFile: mockExecFile,32}));3334vi.mock('../../../../../util/vs/base/common/platform', () => ({35get isWindows() { return mockIsWindows.value; },36}));3738import { CopilotCLISessionTracker, getParentPid } from '../copilotCLISessionTracker';3940function fireTerminalClose(terminal: MockTerminal): void {41for (const listener of terminalCloseListeners) {42listener(terminal);43}44}4546describe('CopilotCLISessionTracker', () => {47let tracker: CopilotCLISessionTracker;4849beforeEach(() => {50tracker?.dispose();51tracker = new CopilotCLISessionTracker();52mockTerminals.value = [];53mockIsWindows.value = false;54// Default: getParentPid fails (process not found), so grandparent fallback is a no-op55mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {56callback(new Error('process not found'), '', '');57});58});5960describe('registerSession', () => {61it('should register a session with pid and ppid', () => {62const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });63expect(disposable).toBeDefined();64expect(disposable.dispose).toBeInstanceOf(Function);65});6667it('should remove session on dispose', async () => {68const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });69mockTerminals.value = [70{ processId: Promise.resolve(5678), name: 'terminal-1' },71];7273// Terminal should be found before dispose74const terminalBefore = await tracker.getTerminal('session-1');75expect(terminalBefore).toBeDefined();7677disposable.dispose();7879// Terminal should not be found after dispose80const terminalAfter = await tracker.getTerminal('session-1');81expect(terminalAfter).toBeUndefined();82});8384it('should overwrite existing session with same id', async () => {85tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });86tracker.registerSession('session-1', { pid: 3000, ppid: 4000 });8788mockTerminals.value = [89{ processId: Promise.resolve(2000), name: 'terminal-old' },90{ processId: Promise.resolve(4000), name: 'terminal-new' },91];9293const terminal = await tracker.getTerminal('session-1');94// Should match the new ppid (4000), not the old one (2000)95expect(terminal).toBeDefined();96expect((terminal as { name: string }).name).toBe('terminal-new');97});98});99100describe('getTerminal', () => {101it('should return undefined for unknown session', async () => {102const terminal = await tracker.getTerminal('unknown-session');103expect(terminal).toBeUndefined();104});105106it('should return undefined when no terminals exist', async () => {107tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });108mockTerminals.value = [];109110const terminal = await tracker.getTerminal('session-1');111expect(terminal).toBeUndefined();112});113114it('should find terminal matching session ppid', async () => {115tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });116const expectedTerminal = { processId: Promise.resolve(5678), name: 'matching-terminal' };117mockTerminals.value = [118{ processId: Promise.resolve(1111), name: 'other-terminal' },119expectedTerminal,120{ processId: Promise.resolve(9999), name: 'another-terminal' },121];122123const terminal = await tracker.getTerminal('session-1');124expect(terminal).toBe(expectedTerminal);125});126127it('should return undefined when no terminal matches ppid', async () => {128tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });129mockTerminals.value = [130{ processId: Promise.resolve(1111), name: 'terminal-1' },131{ processId: Promise.resolve(2222), name: 'terminal-2' },132];133134const terminal = await tracker.getTerminal('session-1');135expect(terminal).toBeUndefined();136});137138it('should handle terminals with undefined processId', async () => {139tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });140mockTerminals.value = [141{ processId: Promise.resolve(undefined as unknown as number), name: 'no-pid-terminal' },142{ processId: Promise.resolve(5678), name: 'matching-terminal' },143];144145const terminal = await tracker.getTerminal('session-1');146expect(terminal).toBeDefined();147expect((terminal as { name: string }).name).toBe('matching-terminal');148});149150it('should return first matching terminal when multiple match', async () => {151tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });152const firstMatch = { processId: Promise.resolve(5678), name: 'first-match' };153const secondMatch = { processId: Promise.resolve(5678), name: 'second-match' };154mockTerminals.value = [firstMatch, secondMatch];155156const terminal = await tracker.getTerminal('session-1');157expect(terminal).toBe(firstMatch);158});159160it('should find correct terminal for different sessions', async () => {161tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });162tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });163164const terminal1 = { processId: Promise.resolve(2000), name: 'terminal-for-session-1' };165const terminal2 = { processId: Promise.resolve(4000), name: 'terminal-for-session-2' };166mockTerminals.value = [terminal1, terminal2];167168const result1 = await tracker.getTerminal('session-1');169const result2 = await tracker.getTerminal('session-2');170expect(result1).toBe(terminal1);171expect(result2).toBe(terminal2);172});173});174175describe('setSessionName and getSessionDisplayName', () => {176it('should return sessionId when no name is set', () => {177tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });178expect(tracker.getSessionDisplayName('session-1')).toBe('Copilot CLI Session');179});180181it('should return sessionId when name is empty string', () => {182tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });183tracker.setSessionName('session-1', '');184expect(tracker.getSessionDisplayName('session-1')).toBe('Copilot CLI Session');185});186187it('should return custom name after setSessionName', () => {188tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });189tracker.setSessionName('session-1', 'Fix Login Bug');190expect(tracker.getSessionDisplayName('session-1')).toBe('Fix Login Bug');191});192193it('should update name when setSessionName called multiple times', () => {194tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });195tracker.setSessionName('session-1', 'First Name');196tracker.setSessionName('session-1', 'Second Name');197expect(tracker.getSessionDisplayName('session-1')).toBe('Second Name');198});199200it('should clear name when session is disposed', () => {201const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });202tracker.setSessionName('session-1', 'My Session');203expect(tracker.getSessionDisplayName('session-1')).toBe('My Session');204205disposable.dispose();206expect(tracker.getSessionDisplayName('session-1')).toBe('Copilot CLI Session');207});208209it('should track names independently for different sessions', () => {210tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });211tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });212213tracker.setSessionName('session-1', 'Session One');214tracker.setSessionName('session-2', 'Session Two');215216expect(tracker.getSessionDisplayName('session-1')).toBe('Session One');217expect(tracker.getSessionDisplayName('session-2')).toBe('Session Two');218});219});220221describe('dispose lifecycle', () => {222it('disposing first registration does not affect second registration with different id', async () => {223const disposable1 = tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });224tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });225226disposable1.dispose();227228mockTerminals.value = [229{ processId: Promise.resolve(4000), name: 'terminal-2' },230];231232// session-1 should be gone233const terminal1 = await tracker.getTerminal('session-1');234expect(terminal1).toBeUndefined();235236// session-2 should still work237const terminal2 = await tracker.getTerminal('session-2');238expect(terminal2).toBeDefined();239});240241it('disposing overwritten registration removes the session', async () => {242const disposable1 = tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });243const disposable2 = tracker.registerSession('session-1', { pid: 3000, ppid: 4000 });244245// Disposing the second registration should remove the session246disposable2.dispose();247248mockTerminals.value = [249{ processId: Promise.resolve(4000), name: 'terminal-new' },250];251252const terminal = await tracker.getTerminal('session-1');253expect(terminal).toBeUndefined();254255// Disposing the first (already overwritten) should be a no-op256disposable1.dispose();257});258});259260describe('setSessionTerminal', () => {261it('should return directly-set terminal from getTerminal', async () => {262tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });263const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;264tracker.setSessionTerminal('session-1', directTerminal as any);265266const result = await tracker.getTerminal('session-1');267expect(result).toBe(directTerminal);268});269270it('should take priority over PID matching', async () => {271tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });272const pidTerminal = { processId: Promise.resolve(5678), name: 'pid-terminal' };273const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;274mockTerminals.value = [pidTerminal];275276tracker.setSessionTerminal('session-1', directTerminal as any);277278const result = await tracker.getTerminal('session-1');279expect(result).toBe(directTerminal);280});281282it('should remove mapping when terminal is closed', async () => {283tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });284const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;285tracker.setSessionTerminal('session-1', directTerminal as any);286287// Verify it's set288expect(await tracker.getTerminal('session-1')).toBe(directTerminal);289290// Fire terminal close291fireTerminalClose(directTerminal);292293// Should fall back to PID lookup (which returns undefined since no terminals match ppid)294mockTerminals.value = [];295expect(await tracker.getTerminal('session-1')).toBeUndefined();296});297298it('should fall back to PID matching after terminal close', async () => {299tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });300const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;301const pidTerminal = { processId: Promise.resolve(5678), name: 'pid-terminal' };302tracker.setSessionTerminal('session-1', directTerminal as any);303mockTerminals.value = [pidTerminal];304305// Fire terminal close for the direct terminal306fireTerminalClose(directTerminal);307308// Should now fall back to PID-based lookup309const result = await tracker.getTerminal('session-1');310expect(result).toBe(pidTerminal);311});312313it('should remove mapping when session is disposed', async () => {314const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });315const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;316tracker.setSessionTerminal('session-1', directTerminal as any);317318expect(await tracker.getTerminal('session-1')).toBe(directTerminal);319320disposable.dispose();321322expect(await tracker.getTerminal('session-1')).toBeUndefined();323});324325it('should track terminals independently for different sessions', async () => {326tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });327tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });328329const terminal1 = { processId: Promise.resolve(9991), name: 'terminal-1' } as MockTerminal;330const terminal2 = { processId: Promise.resolve(9992), name: 'terminal-2' } as MockTerminal;331332tracker.setSessionTerminal('session-1', terminal1 as any);333tracker.setSessionTerminal('session-2', terminal2 as any);334335expect(await tracker.getTerminal('session-1')).toBe(terminal1);336expect(await tracker.getTerminal('session-2')).toBe(terminal2);337});338339it('should only remove mapping for the closed terminal', async () => {340tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });341tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });342343const terminal1 = { processId: Promise.resolve(9991), name: 'terminal-1' } as MockTerminal;344const terminal2 = { processId: Promise.resolve(9992), name: 'terminal-2' } as MockTerminal;345346tracker.setSessionTerminal('session-1', terminal1 as any);347tracker.setSessionTerminal('session-2', terminal2 as any);348349// Close only terminal1350fireTerminalClose(terminal1);351352// session-1 should lose its direct mapping353mockTerminals.value = [];354expect(await tracker.getTerminal('session-1')).toBeUndefined();355// session-2 should still have its direct mapping356expect(await tracker.getTerminal('session-2')).toBe(terminal2);357});358359it('should overwrite previous terminal for same session', async () => {360tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });361362const terminal1 = { processId: Promise.resolve(9991), name: 'terminal-1' } as MockTerminal;363const terminal2 = { processId: Promise.resolve(9992), name: 'terminal-2' } as MockTerminal;364365tracker.setSessionTerminal('session-1', terminal1 as any);366tracker.setSessionTerminal('session-1', terminal2 as any);367368expect(await tracker.getTerminal('session-1')).toBe(terminal2);369});370});371372describe('getTerminal grandparent fallback', () => {373beforeEach(() => {374mockExecFile.mockClear();375});376377it('should fall back to grandparent PID when no direct PPID match', async () => {378tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });379380// No terminal matches ppid 5678, but grandparent is 9999381const grandparentTerminal = { processId: Promise.resolve(9999), name: 'grandparent-terminal' };382mockTerminals.value = [grandparentTerminal];383384// Mock getParentPid(5678) -> 9999385mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {386callback(null, ' 9999\n', '');387});388389const result = await tracker.getTerminal('session-1');390expect(result).toBe(grandparentTerminal);391});392393it('should return undefined when both PPID and grandparent fail', async () => {394tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });395mockTerminals.value = [396{ processId: Promise.resolve(1111), name: 'unrelated-terminal' },397];398399// getParentPid fails400mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {401callback(new Error('process not found'), '', '');402});403404const result = await tracker.getTerminal('session-1');405expect(result).toBeUndefined();406});407408it('should not call getParentPid when direct PPID match succeeds', async () => {409tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });410const ppidTerminal = { processId: Promise.resolve(5678), name: 'ppid-terminal' };411mockTerminals.value = [ppidTerminal];412413const result = await tracker.getTerminal('session-1');414expect(result).toBe(ppidTerminal);415// execFile should not have been called since PPID matched directly416expect(mockExecFile).not.toHaveBeenCalled();417});418419it('should not call getParentPid when direct terminal mapping exists', async () => {420tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });421const directTerminal = { processId: Promise.resolve(7777), name: 'direct' } as MockTerminal;422tracker.setSessionTerminal('session-1', directTerminal as any);423424const result = await tracker.getTerminal('session-1');425expect(result).toBe(directTerminal);426expect(mockExecFile).not.toHaveBeenCalled();427});428429it('should return undefined when grandparent PID matches no terminal', async () => {430tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });431mockTerminals.value = [432{ processId: Promise.resolve(1111), name: 'unrelated-terminal' },433];434435// getParentPid returns a PID that no terminal matches436mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {437callback(null, '2222\n', '');438});439440const result = await tracker.getTerminal('session-1');441expect(result).toBeUndefined();442});443444it('should walk multiple generations to find a terminal', async () => {445tracker.registerSession('session-1', { pid: 1234, ppid: 100 });446447// Terminal has PID 400 (great-great-grandparent)448const ancestorTerminal = { processId: Promise.resolve(400), name: 'ancestor-terminal' };449mockTerminals.value = [ancestorTerminal];450451// Chain: 100 -> 200 -> 300 -> 400452mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {453const pid = args[args.length - 1];454const chain: Record<string, string> = { '100': '200', '200': '300', '300': '400' };455if (chain[pid]) {456callback(null, `${chain[pid]}\n`, '');457} else {458callback(new Error('not found'), '', '');459}460});461462const result = await tracker.getTerminal('session-1');463expect(result).toBe(ancestorTerminal);464});465466it('should stop walking after 4 generations', async () => {467tracker.registerSession('session-1', { pid: 1234, ppid: 100 });468469// Terminal has PID 600 (5th generation — too far)470const farTerminal = { processId: Promise.resolve(600), name: 'far-terminal' };471mockTerminals.value = [farTerminal];472473// Chain: 100 -> 200 -> 300 -> 400 -> 500 -> 600474mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {475const pid = args[args.length - 1];476const chain: Record<string, string> = { '100': '200', '200': '300', '300': '400', '400': '500', '500': '600' };477if (chain[pid]) {478callback(null, `${chain[pid]}\n`, '');479} else {480callback(new Error('not found'), '', '');481}482});483484const result = await tracker.getTerminal('session-1');485expect(result).toBeUndefined();486// Should have called getParentPid exactly 4 times (generations 1-4)487expect(mockExecFile).toHaveBeenCalledTimes(4);488});489490it('should cache ancestor PIDs and reuse them on subsequent calls', async () => {491tracker.registerSession('session-1', { pid: 1234, ppid: 100 });492493// First call: no terminal matches anything494mockTerminals.value = [];495mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {496const pid = args[args.length - 1];497const chain: Record<string, string> = { '100': '200', '200': '300' };498if (chain[pid]) {499callback(null, `${chain[pid]}\n`, '');500} else {501callback(new Error('not found'), '', '');502}503});504505await tracker.getTerminal('session-1');506const firstCallCount = mockExecFile.mock.calls.length;507expect(firstCallCount).toBeGreaterThan(0);508509// Second call: terminal now matches grandparent PID 200510mockExecFile.mockClear();511const terminal = { processId: Promise.resolve(200), name: 'grandparent-terminal' };512mockTerminals.value = [terminal];513514const result = await tracker.getTerminal('session-1');515expect(result).toBe(terminal);516// Should not call execFile again — PIDs 200 and 300 are cached517expect(mockExecFile).not.toHaveBeenCalled();518});519520it('should store found terminal in _sessionTerminals for faster future lookups', async () => {521tracker.registerSession('session-1', { pid: 1234, ppid: 100 });522523const ancestorTerminal = { processId: Promise.resolve(200), name: 'ancestor-terminal' };524mockTerminals.value = [ancestorTerminal];525526mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {527callback(null, '200\n', '');528});529530// First call: finds terminal via ancestor walk531const result1 = await tracker.getTerminal('session-1');532expect(result1).toBe(ancestorTerminal);533534mockExecFile.mockClear();535536// Second call: should return immediately from _sessionTerminals (direct mapping)537const result2 = await tracker.getTerminal('session-1');538expect(result2).toBe(ancestorTerminal);539// No ancestor walking needed540expect(mockExecFile).not.toHaveBeenCalled();541});542543it('should stop walking when getParentPid returns undefined', async () => {544tracker.registerSession('session-1', { pid: 1234, ppid: 100 });545mockTerminals.value = [];546547// Only one generation available: 100 -> 200, then fails548mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {549const pid = args[args.length - 1];550if (pid === '100') {551callback(null, '200\n', '');552} else {553callback(new Error('not found'), '', '');554}555});556557const result = await tracker.getTerminal('session-1');558expect(result).toBeUndefined();559// Should have called getParentPid twice: once for 100->200, once for 200->fail560expect(mockExecFile).toHaveBeenCalledTimes(2);561});562563it('should clear cached ancestor PIDs when session is disposed', async () => {564const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 100 });565mockTerminals.value = [];566567mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {568callback(null, '200\n', '');569});570571// Populate cache572await tracker.getTerminal('session-1');573574disposable.dispose();575mockExecFile.mockClear();576577// Re-register and call again — should need to re-fetch578tracker.registerSession('session-1', { pid: 1234, ppid: 100 });579await tracker.getTerminal('session-1');580expect(mockExecFile).toHaveBeenCalled();581});582});583});584585describe('getParentPid', () => {586beforeEach(() => {587mockExecFile.mockClear();588mockIsWindows.value = false;589});590591describe('on Linux/macOS', () => {592it('should return the parent PID from ps output', async () => {593mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {594callback(null, ' 1234\n', '');595});596597const result = await getParentPid(5678);598expect(result).toBe(1234);599expect(mockExecFile).toHaveBeenCalledWith('ps', ['-o', 'ppid=', '-p', '5678'], { windowsHide: true }, expect.any(Function));600});601602it('should return undefined when ps fails', async () => {603mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {604callback(new Error('No such process'), '', '');605});606607const result = await getParentPid(99999);608expect(result).toBeUndefined();609});610611it('should return undefined when ps returns non-numeric output', async () => {612mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {613callback(null, '', '');614});615616const result = await getParentPid(5678);617expect(result).toBeUndefined();618});619});620621describe('on Windows', () => {622beforeEach(() => {623mockIsWindows.value = true;624});625626it('should return the parent PID from PowerShell output', async () => {627mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {628callback(null, '5678\r\n', '');629});630631const result = await getParentPid(1234);632expect(result).toBe(5678);633expect(mockExecFile).toHaveBeenCalledWith(634'powershell.exe',635['-NoProfile', '-Command', '(Get-CimInstance Win32_Process -Filter \"ProcessId=1234\").ParentProcessId'],636{ windowsHide: true },637expect.any(Function)638);639});640641it('should return undefined when PowerShell fails', async () => {642mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {643callback(new Error('PowerShell error'), '', '');644});645646const result = await getParentPid(1234);647expect(result).toBeUndefined();648});649650it('should return undefined when PowerShell returns empty output', async () => {651mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {652callback(null, '\r\n', '');653});654655const result = await getParentPid(1234);656expect(result).toBeUndefined();657});658});659});660661662