Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/test/copilotCLISessionTracker.spec.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { beforeEach, describe, expect, it, vi } from 'vitest';
7
8
type MockTerminal = { processId: Promise<number | undefined>; name: string };
9
10
const { mockTerminals, terminalCloseListeners, mockExecFile, mockIsWindows } = vi.hoisted(() => ({
11
mockTerminals: { value: [] as Array<MockTerminal> },
12
terminalCloseListeners: [] as Array<(terminal: MockTerminal) => void>,
13
mockExecFile: vi.fn(),
14
mockIsWindows: { value: false },
15
}));
16
17
vi.mock('vscode', async (importOriginal) => {
18
const actual = await importOriginal() as Record<string, unknown>;
19
return {
20
...actual,
21
window: {
22
get terminals() { return mockTerminals.value; },
23
onDidCloseTerminal(listener: (terminal: MockTerminal) => void) {
24
terminalCloseListeners.push(listener);
25
return { dispose: () => { const idx = terminalCloseListeners.indexOf(listener); if (idx >= 0) { terminalCloseListeners.splice(idx, 1); } } };
26
},
27
},
28
};
29
});
30
31
vi.mock('child_process', () => ({
32
execFile: mockExecFile,
33
}));
34
35
vi.mock('../../../../../util/vs/base/common/platform', () => ({
36
get isWindows() { return mockIsWindows.value; },
37
}));
38
39
import { CopilotCLISessionTracker, getParentPid } from '../copilotCLISessionTracker';
40
41
function fireTerminalClose(terminal: MockTerminal): void {
42
for (const listener of terminalCloseListeners) {
43
listener(terminal);
44
}
45
}
46
47
describe('CopilotCLISessionTracker', () => {
48
let tracker: CopilotCLISessionTracker;
49
50
beforeEach(() => {
51
tracker?.dispose();
52
tracker = new CopilotCLISessionTracker();
53
mockTerminals.value = [];
54
mockIsWindows.value = false;
55
// Default: getParentPid fails (process not found), so grandparent fallback is a no-op
56
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
57
callback(new Error('process not found'), '', '');
58
});
59
});
60
61
describe('registerSession', () => {
62
it('should register a session with pid and ppid', () => {
63
const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
64
expect(disposable).toBeDefined();
65
expect(disposable.dispose).toBeInstanceOf(Function);
66
});
67
68
it('should remove session on dispose', async () => {
69
const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
70
mockTerminals.value = [
71
{ processId: Promise.resolve(5678), name: 'terminal-1' },
72
];
73
74
// Terminal should be found before dispose
75
const terminalBefore = await tracker.getTerminal('session-1');
76
expect(terminalBefore).toBeDefined();
77
78
disposable.dispose();
79
80
// Terminal should not be found after dispose
81
const terminalAfter = await tracker.getTerminal('session-1');
82
expect(terminalAfter).toBeUndefined();
83
});
84
85
it('should overwrite existing session with same id', async () => {
86
tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });
87
tracker.registerSession('session-1', { pid: 3000, ppid: 4000 });
88
89
mockTerminals.value = [
90
{ processId: Promise.resolve(2000), name: 'terminal-old' },
91
{ processId: Promise.resolve(4000), name: 'terminal-new' },
92
];
93
94
const terminal = await tracker.getTerminal('session-1');
95
// Should match the new ppid (4000), not the old one (2000)
96
expect(terminal).toBeDefined();
97
expect((terminal as { name: string }).name).toBe('terminal-new');
98
});
99
});
100
101
describe('getTerminal', () => {
102
it('should return undefined for unknown session', async () => {
103
const terminal = await tracker.getTerminal('unknown-session');
104
expect(terminal).toBeUndefined();
105
});
106
107
it('should return undefined when no terminals exist', async () => {
108
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
109
mockTerminals.value = [];
110
111
const terminal = await tracker.getTerminal('session-1');
112
expect(terminal).toBeUndefined();
113
});
114
115
it('should find terminal matching session ppid', async () => {
116
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
117
const expectedTerminal = { processId: Promise.resolve(5678), name: 'matching-terminal' };
118
mockTerminals.value = [
119
{ processId: Promise.resolve(1111), name: 'other-terminal' },
120
expectedTerminal,
121
{ processId: Promise.resolve(9999), name: 'another-terminal' },
122
];
123
124
const terminal = await tracker.getTerminal('session-1');
125
expect(terminal).toBe(expectedTerminal);
126
});
127
128
it('should return undefined when no terminal matches ppid', async () => {
129
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
130
mockTerminals.value = [
131
{ processId: Promise.resolve(1111), name: 'terminal-1' },
132
{ processId: Promise.resolve(2222), name: 'terminal-2' },
133
];
134
135
const terminal = await tracker.getTerminal('session-1');
136
expect(terminal).toBeUndefined();
137
});
138
139
it('should handle terminals with undefined processId', async () => {
140
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
141
mockTerminals.value = [
142
{ processId: Promise.resolve(undefined as unknown as number), name: 'no-pid-terminal' },
143
{ processId: Promise.resolve(5678), name: 'matching-terminal' },
144
];
145
146
const terminal = await tracker.getTerminal('session-1');
147
expect(terminal).toBeDefined();
148
expect((terminal as { name: string }).name).toBe('matching-terminal');
149
});
150
151
it('should return first matching terminal when multiple match', async () => {
152
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
153
const firstMatch = { processId: Promise.resolve(5678), name: 'first-match' };
154
const secondMatch = { processId: Promise.resolve(5678), name: 'second-match' };
155
mockTerminals.value = [firstMatch, secondMatch];
156
157
const terminal = await tracker.getTerminal('session-1');
158
expect(terminal).toBe(firstMatch);
159
});
160
161
it('should find correct terminal for different sessions', async () => {
162
tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });
163
tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });
164
165
const terminal1 = { processId: Promise.resolve(2000), name: 'terminal-for-session-1' };
166
const terminal2 = { processId: Promise.resolve(4000), name: 'terminal-for-session-2' };
167
mockTerminals.value = [terminal1, terminal2];
168
169
const result1 = await tracker.getTerminal('session-1');
170
const result2 = await tracker.getTerminal('session-2');
171
expect(result1).toBe(terminal1);
172
expect(result2).toBe(terminal2);
173
});
174
});
175
176
describe('setSessionName and getSessionDisplayName', () => {
177
it('should return sessionId when no name is set', () => {
178
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
179
expect(tracker.getSessionDisplayName('session-1')).toBe('Copilot CLI Session');
180
});
181
182
it('should return sessionId when name is empty string', () => {
183
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
184
tracker.setSessionName('session-1', '');
185
expect(tracker.getSessionDisplayName('session-1')).toBe('Copilot CLI Session');
186
});
187
188
it('should return custom name after setSessionName', () => {
189
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
190
tracker.setSessionName('session-1', 'Fix Login Bug');
191
expect(tracker.getSessionDisplayName('session-1')).toBe('Fix Login Bug');
192
});
193
194
it('should update name when setSessionName called multiple times', () => {
195
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
196
tracker.setSessionName('session-1', 'First Name');
197
tracker.setSessionName('session-1', 'Second Name');
198
expect(tracker.getSessionDisplayName('session-1')).toBe('Second Name');
199
});
200
201
it('should clear name when session is disposed', () => {
202
const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
203
tracker.setSessionName('session-1', 'My Session');
204
expect(tracker.getSessionDisplayName('session-1')).toBe('My Session');
205
206
disposable.dispose();
207
expect(tracker.getSessionDisplayName('session-1')).toBe('Copilot CLI Session');
208
});
209
210
it('should track names independently for different sessions', () => {
211
tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });
212
tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });
213
214
tracker.setSessionName('session-1', 'Session One');
215
tracker.setSessionName('session-2', 'Session Two');
216
217
expect(tracker.getSessionDisplayName('session-1')).toBe('Session One');
218
expect(tracker.getSessionDisplayName('session-2')).toBe('Session Two');
219
});
220
});
221
222
describe('dispose lifecycle', () => {
223
it('disposing first registration does not affect second registration with different id', async () => {
224
const disposable1 = tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });
225
tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });
226
227
disposable1.dispose();
228
229
mockTerminals.value = [
230
{ processId: Promise.resolve(4000), name: 'terminal-2' },
231
];
232
233
// session-1 should be gone
234
const terminal1 = await tracker.getTerminal('session-1');
235
expect(terminal1).toBeUndefined();
236
237
// session-2 should still work
238
const terminal2 = await tracker.getTerminal('session-2');
239
expect(terminal2).toBeDefined();
240
});
241
242
it('disposing overwritten registration removes the session', async () => {
243
const disposable1 = tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });
244
const disposable2 = tracker.registerSession('session-1', { pid: 3000, ppid: 4000 });
245
246
// Disposing the second registration should remove the session
247
disposable2.dispose();
248
249
mockTerminals.value = [
250
{ processId: Promise.resolve(4000), name: 'terminal-new' },
251
];
252
253
const terminal = await tracker.getTerminal('session-1');
254
expect(terminal).toBeUndefined();
255
256
// Disposing the first (already overwritten) should be a no-op
257
disposable1.dispose();
258
});
259
});
260
261
describe('setSessionTerminal', () => {
262
it('should return directly-set terminal from getTerminal', async () => {
263
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
264
const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;
265
tracker.setSessionTerminal('session-1', directTerminal as any);
266
267
const result = await tracker.getTerminal('session-1');
268
expect(result).toBe(directTerminal);
269
});
270
271
it('should take priority over PID matching', async () => {
272
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
273
const pidTerminal = { processId: Promise.resolve(5678), name: 'pid-terminal' };
274
const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;
275
mockTerminals.value = [pidTerminal];
276
277
tracker.setSessionTerminal('session-1', directTerminal as any);
278
279
const result = await tracker.getTerminal('session-1');
280
expect(result).toBe(directTerminal);
281
});
282
283
it('should remove mapping when terminal is closed', async () => {
284
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
285
const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;
286
tracker.setSessionTerminal('session-1', directTerminal as any);
287
288
// Verify it's set
289
expect(await tracker.getTerminal('session-1')).toBe(directTerminal);
290
291
// Fire terminal close
292
fireTerminalClose(directTerminal);
293
294
// Should fall back to PID lookup (which returns undefined since no terminals match ppid)
295
mockTerminals.value = [];
296
expect(await tracker.getTerminal('session-1')).toBeUndefined();
297
});
298
299
it('should fall back to PID matching after terminal close', async () => {
300
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
301
const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;
302
const pidTerminal = { processId: Promise.resolve(5678), name: 'pid-terminal' };
303
tracker.setSessionTerminal('session-1', directTerminal as any);
304
mockTerminals.value = [pidTerminal];
305
306
// Fire terminal close for the direct terminal
307
fireTerminalClose(directTerminal);
308
309
// Should now fall back to PID-based lookup
310
const result = await tracker.getTerminal('session-1');
311
expect(result).toBe(pidTerminal);
312
});
313
314
it('should remove mapping when session is disposed', async () => {
315
const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
316
const directTerminal = { processId: Promise.resolve(9999), name: 'direct-terminal' } as MockTerminal;
317
tracker.setSessionTerminal('session-1', directTerminal as any);
318
319
expect(await tracker.getTerminal('session-1')).toBe(directTerminal);
320
321
disposable.dispose();
322
323
expect(await tracker.getTerminal('session-1')).toBeUndefined();
324
});
325
326
it('should track terminals independently for different sessions', async () => {
327
tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });
328
tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });
329
330
const terminal1 = { processId: Promise.resolve(9991), name: 'terminal-1' } as MockTerminal;
331
const terminal2 = { processId: Promise.resolve(9992), name: 'terminal-2' } as MockTerminal;
332
333
tracker.setSessionTerminal('session-1', terminal1 as any);
334
tracker.setSessionTerminal('session-2', terminal2 as any);
335
336
expect(await tracker.getTerminal('session-1')).toBe(terminal1);
337
expect(await tracker.getTerminal('session-2')).toBe(terminal2);
338
});
339
340
it('should only remove mapping for the closed terminal', async () => {
341
tracker.registerSession('session-1', { pid: 1000, ppid: 2000 });
342
tracker.registerSession('session-2', { pid: 3000, ppid: 4000 });
343
344
const terminal1 = { processId: Promise.resolve(9991), name: 'terminal-1' } as MockTerminal;
345
const terminal2 = { processId: Promise.resolve(9992), name: 'terminal-2' } as MockTerminal;
346
347
tracker.setSessionTerminal('session-1', terminal1 as any);
348
tracker.setSessionTerminal('session-2', terminal2 as any);
349
350
// Close only terminal1
351
fireTerminalClose(terminal1);
352
353
// session-1 should lose its direct mapping
354
mockTerminals.value = [];
355
expect(await tracker.getTerminal('session-1')).toBeUndefined();
356
// session-2 should still have its direct mapping
357
expect(await tracker.getTerminal('session-2')).toBe(terminal2);
358
});
359
360
it('should overwrite previous terminal for same session', async () => {
361
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
362
363
const terminal1 = { processId: Promise.resolve(9991), name: 'terminal-1' } as MockTerminal;
364
const terminal2 = { processId: Promise.resolve(9992), name: 'terminal-2' } as MockTerminal;
365
366
tracker.setSessionTerminal('session-1', terminal1 as any);
367
tracker.setSessionTerminal('session-1', terminal2 as any);
368
369
expect(await tracker.getTerminal('session-1')).toBe(terminal2);
370
});
371
});
372
373
describe('getTerminal grandparent fallback', () => {
374
beforeEach(() => {
375
mockExecFile.mockClear();
376
});
377
378
it('should fall back to grandparent PID when no direct PPID match', async () => {
379
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
380
381
// No terminal matches ppid 5678, but grandparent is 9999
382
const grandparentTerminal = { processId: Promise.resolve(9999), name: 'grandparent-terminal' };
383
mockTerminals.value = [grandparentTerminal];
384
385
// Mock getParentPid(5678) -> 9999
386
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
387
callback(null, ' 9999\n', '');
388
});
389
390
const result = await tracker.getTerminal('session-1');
391
expect(result).toBe(grandparentTerminal);
392
});
393
394
it('should return undefined when both PPID and grandparent fail', async () => {
395
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
396
mockTerminals.value = [
397
{ processId: Promise.resolve(1111), name: 'unrelated-terminal' },
398
];
399
400
// getParentPid fails
401
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
402
callback(new Error('process not found'), '', '');
403
});
404
405
const result = await tracker.getTerminal('session-1');
406
expect(result).toBeUndefined();
407
});
408
409
it('should not call getParentPid when direct PPID match succeeds', async () => {
410
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
411
const ppidTerminal = { processId: Promise.resolve(5678), name: 'ppid-terminal' };
412
mockTerminals.value = [ppidTerminal];
413
414
const result = await tracker.getTerminal('session-1');
415
expect(result).toBe(ppidTerminal);
416
// execFile should not have been called since PPID matched directly
417
expect(mockExecFile).not.toHaveBeenCalled();
418
});
419
420
it('should not call getParentPid when direct terminal mapping exists', async () => {
421
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
422
const directTerminal = { processId: Promise.resolve(7777), name: 'direct' } as MockTerminal;
423
tracker.setSessionTerminal('session-1', directTerminal as any);
424
425
const result = await tracker.getTerminal('session-1');
426
expect(result).toBe(directTerminal);
427
expect(mockExecFile).not.toHaveBeenCalled();
428
});
429
430
it('should return undefined when grandparent PID matches no terminal', async () => {
431
tracker.registerSession('session-1', { pid: 1234, ppid: 5678 });
432
mockTerminals.value = [
433
{ processId: Promise.resolve(1111), name: 'unrelated-terminal' },
434
];
435
436
// getParentPid returns a PID that no terminal matches
437
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
438
callback(null, '2222\n', '');
439
});
440
441
const result = await tracker.getTerminal('session-1');
442
expect(result).toBeUndefined();
443
});
444
445
it('should walk multiple generations to find a terminal', async () => {
446
tracker.registerSession('session-1', { pid: 1234, ppid: 100 });
447
448
// Terminal has PID 400 (great-great-grandparent)
449
const ancestorTerminal = { processId: Promise.resolve(400), name: 'ancestor-terminal' };
450
mockTerminals.value = [ancestorTerminal];
451
452
// Chain: 100 -> 200 -> 300 -> 400
453
mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
454
const pid = args[args.length - 1];
455
const chain: Record<string, string> = { '100': '200', '200': '300', '300': '400' };
456
if (chain[pid]) {
457
callback(null, `${chain[pid]}\n`, '');
458
} else {
459
callback(new Error('not found'), '', '');
460
}
461
});
462
463
const result = await tracker.getTerminal('session-1');
464
expect(result).toBe(ancestorTerminal);
465
});
466
467
it('should stop walking after 4 generations', async () => {
468
tracker.registerSession('session-1', { pid: 1234, ppid: 100 });
469
470
// Terminal has PID 600 (5th generation — too far)
471
const farTerminal = { processId: Promise.resolve(600), name: 'far-terminal' };
472
mockTerminals.value = [farTerminal];
473
474
// Chain: 100 -> 200 -> 300 -> 400 -> 500 -> 600
475
mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
476
const pid = args[args.length - 1];
477
const chain: Record<string, string> = { '100': '200', '200': '300', '300': '400', '400': '500', '500': '600' };
478
if (chain[pid]) {
479
callback(null, `${chain[pid]}\n`, '');
480
} else {
481
callback(new Error('not found'), '', '');
482
}
483
});
484
485
const result = await tracker.getTerminal('session-1');
486
expect(result).toBeUndefined();
487
// Should have called getParentPid exactly 4 times (generations 1-4)
488
expect(mockExecFile).toHaveBeenCalledTimes(4);
489
});
490
491
it('should cache ancestor PIDs and reuse them on subsequent calls', async () => {
492
tracker.registerSession('session-1', { pid: 1234, ppid: 100 });
493
494
// First call: no terminal matches anything
495
mockTerminals.value = [];
496
mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
497
const pid = args[args.length - 1];
498
const chain: Record<string, string> = { '100': '200', '200': '300' };
499
if (chain[pid]) {
500
callback(null, `${chain[pid]}\n`, '');
501
} else {
502
callback(new Error('not found'), '', '');
503
}
504
});
505
506
await tracker.getTerminal('session-1');
507
const firstCallCount = mockExecFile.mock.calls.length;
508
expect(firstCallCount).toBeGreaterThan(0);
509
510
// Second call: terminal now matches grandparent PID 200
511
mockExecFile.mockClear();
512
const terminal = { processId: Promise.resolve(200), name: 'grandparent-terminal' };
513
mockTerminals.value = [terminal];
514
515
const result = await tracker.getTerminal('session-1');
516
expect(result).toBe(terminal);
517
// Should not call execFile again — PIDs 200 and 300 are cached
518
expect(mockExecFile).not.toHaveBeenCalled();
519
});
520
521
it('should store found terminal in _sessionTerminals for faster future lookups', async () => {
522
tracker.registerSession('session-1', { pid: 1234, ppid: 100 });
523
524
const ancestorTerminal = { processId: Promise.resolve(200), name: 'ancestor-terminal' };
525
mockTerminals.value = [ancestorTerminal];
526
527
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
528
callback(null, '200\n', '');
529
});
530
531
// First call: finds terminal via ancestor walk
532
const result1 = await tracker.getTerminal('session-1');
533
expect(result1).toBe(ancestorTerminal);
534
535
mockExecFile.mockClear();
536
537
// Second call: should return immediately from _sessionTerminals (direct mapping)
538
const result2 = await tracker.getTerminal('session-1');
539
expect(result2).toBe(ancestorTerminal);
540
// No ancestor walking needed
541
expect(mockExecFile).not.toHaveBeenCalled();
542
});
543
544
it('should stop walking when getParentPid returns undefined', async () => {
545
tracker.registerSession('session-1', { pid: 1234, ppid: 100 });
546
mockTerminals.value = [];
547
548
// Only one generation available: 100 -> 200, then fails
549
mockExecFile.mockImplementation((_cmd: string, args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
550
const pid = args[args.length - 1];
551
if (pid === '100') {
552
callback(null, '200\n', '');
553
} else {
554
callback(new Error('not found'), '', '');
555
}
556
});
557
558
const result = await tracker.getTerminal('session-1');
559
expect(result).toBeUndefined();
560
// Should have called getParentPid twice: once for 100->200, once for 200->fail
561
expect(mockExecFile).toHaveBeenCalledTimes(2);
562
});
563
564
it('should clear cached ancestor PIDs when session is disposed', async () => {
565
const disposable = tracker.registerSession('session-1', { pid: 1234, ppid: 100 });
566
mockTerminals.value = [];
567
568
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
569
callback(null, '200\n', '');
570
});
571
572
// Populate cache
573
await tracker.getTerminal('session-1');
574
575
disposable.dispose();
576
mockExecFile.mockClear();
577
578
// Re-register and call again — should need to re-fetch
579
tracker.registerSession('session-1', { pid: 1234, ppid: 100 });
580
await tracker.getTerminal('session-1');
581
expect(mockExecFile).toHaveBeenCalled();
582
});
583
});
584
});
585
586
describe('getParentPid', () => {
587
beforeEach(() => {
588
mockExecFile.mockClear();
589
mockIsWindows.value = false;
590
});
591
592
describe('on Linux/macOS', () => {
593
it('should return the parent PID from ps output', async () => {
594
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
595
callback(null, ' 1234\n', '');
596
});
597
598
const result = await getParentPid(5678);
599
expect(result).toBe(1234);
600
expect(mockExecFile).toHaveBeenCalledWith('ps', ['-o', 'ppid=', '-p', '5678'], { windowsHide: true }, expect.any(Function));
601
});
602
603
it('should return undefined when ps fails', async () => {
604
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
605
callback(new Error('No such process'), '', '');
606
});
607
608
const result = await getParentPid(99999);
609
expect(result).toBeUndefined();
610
});
611
612
it('should return undefined when ps returns non-numeric output', async () => {
613
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
614
callback(null, '', '');
615
});
616
617
const result = await getParentPid(5678);
618
expect(result).toBeUndefined();
619
});
620
});
621
622
describe('on Windows', () => {
623
beforeEach(() => {
624
mockIsWindows.value = true;
625
});
626
627
it('should return the parent PID from PowerShell output', async () => {
628
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
629
callback(null, '5678\r\n', '');
630
});
631
632
const result = await getParentPid(1234);
633
expect(result).toBe(5678);
634
expect(mockExecFile).toHaveBeenCalledWith(
635
'powershell.exe',
636
['-NoProfile', '-Command', '(Get-CimInstance Win32_Process -Filter \"ProcessId=1234\").ParentProcessId'],
637
{ windowsHide: true },
638
expect.any(Function)
639
);
640
});
641
642
it('should return undefined when PowerShell fails', async () => {
643
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
644
callback(new Error('PowerShell error'), '', '');
645
});
646
647
const result = await getParentPid(1234);
648
expect(result).toBeUndefined();
649
});
650
651
it('should return undefined when PowerShell returns empty output', async () => {
652
mockExecFile.mockImplementation((_cmd: string, _args: string[], _opts: unknown, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
653
callback(null, '\r\n', '');
654
});
655
656
const result = await getParentPid(1234);
657
expect(result).toBeUndefined();
658
});
659
});
660
});
661
662