@ai-devkit/agent-manager 0.26.2 → 0.26.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AgentManager.d.ts +5 -2
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +28 -10
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +289 -4
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +13 -4
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CodexAdapter.test.js +13 -4
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CopilotAdapter.test.js +7 -2
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js +18 -5
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GrokCliAdapter.test.js +7 -2
- package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +13 -4
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/PiAdapter.test.js +32 -4
- package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -1
- package/dist/__tests__/utils/AgentRegistry.test.js +140 -0
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
- package/dist/__tests__/utils/process.test.js +90 -2
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +9 -3
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +5 -4
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +11 -7
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +5 -4
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +11 -7
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/CopilotAdapter.d.ts +5 -4
- package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
- package/dist/adapters/CopilotAdapter.js +11 -7
- package/dist/adapters/CopilotAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +6 -5
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +12 -8
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/adapters/GrokCliAdapter.d.ts +3 -2
- package/dist/adapters/GrokCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GrokCliAdapter.js +10 -6
- package/dist/adapters/GrokCliAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.d.ts +5 -4
- package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +11 -7
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/PiAdapter.d.ts +3 -2
- package/dist/adapters/PiAdapter.d.ts.map +1 -1
- package/dist/adapters/PiAdapter.js +12 -9
- package/dist/adapters/PiAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/database/connection.d.ts +1 -1
- package/dist/database/connection.d.ts.map +1 -1
- package/dist/database/connection.js +1 -1
- package/dist/database/connection.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts +14 -1
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +65 -20
- package/dist/utils/AgentRegistry.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/process.d.ts +9 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +155 -10
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +35 -8
- package/src/__tests__/AgentManager.test.ts +231 -4
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +16 -5
- package/src/__tests__/adapters/CodexAdapter.test.ts +16 -5
- package/src/__tests__/adapters/CopilotAdapter.test.ts +8 -1
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +20 -4
- package/src/__tests__/adapters/GrokCliAdapter.test.ts +8 -1
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +16 -5
- package/src/__tests__/adapters/PiAdapter.test.ts +28 -5
- package/src/__tests__/utils/AgentRegistry.test.ts +119 -0
- package/src/__tests__/utils/process.test.ts +72 -1
- package/src/adapters/AgentAdapter.ts +11 -3
- package/src/adapters/ClaudeCodeAdapter.ts +10 -7
- package/src/adapters/CodexAdapter.ts +10 -7
- package/src/adapters/CopilotAdapter.ts +16 -7
- package/src/adapters/GeminiCliAdapter.ts +17 -8
- package/src/adapters/GrokCliAdapter.ts +9 -6
- package/src/adapters/OpenCodeAdapter.ts +10 -7
- package/src/adapters/PiAdapter.ts +10 -9
- package/src/adapters/index.ts +1 -1
- package/src/database/connection.ts +4 -2
- package/src/index.ts +3 -1
- package/src/utils/AgentRegistry.ts +85 -17
- package/src/utils/index.ts +1 -1
- package/src/utils/process.ts +160 -10
|
@@ -9,16 +9,21 @@ import * as path from 'path';
|
|
|
9
9
|
import { ClaudeCodeAdapter } from '../../adapters/ClaudeCodeAdapter.js';
|
|
10
10
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
11
11
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
12
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
12
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
13
13
|
import { batchGetSessionFileBirthtimes } from '../../utils/session.js';
|
|
14
14
|
import type { SessionFile } from '../../utils/session.js';
|
|
15
15
|
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
16
16
|
import type { MatchResult } from '../../utils/matching.js';
|
|
17
17
|
import * as os from 'os';
|
|
18
|
-
vi.mock('../../utils/process.js', () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal() as typeof import('../../utils/process.js');
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
listAgentProcesses: vi.fn(),
|
|
23
|
+
enrichProcesses: vi.fn(),
|
|
24
|
+
captureProcessSnapshot: vi.fn(),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
22
27
|
|
|
23
28
|
vi.mock('../../utils/session.js', async () => {
|
|
24
29
|
const actual = await vi.importActual('../../utils/session') as typeof import('../../utils/session');
|
|
@@ -35,6 +40,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
35
40
|
|
|
36
41
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
37
42
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
43
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
38
44
|
const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as MockedFunction<typeof batchGetSessionFileBirthtimes>;
|
|
39
45
|
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
40
46
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
@@ -45,11 +51,16 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
45
51
|
adapter = new ClaudeCodeAdapter();
|
|
46
52
|
mockedListAgentProcesses.mockReset();
|
|
47
53
|
mockedEnrichProcesses.mockReset();
|
|
54
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
48
55
|
mockedBatchGetSessionFileBirthtimes.mockReset();
|
|
49
56
|
mockedMatchProcessesToSessions.mockReset();
|
|
50
57
|
mockedGenerateAgentName.mockReset();
|
|
51
58
|
// Default: enrichProcesses returns what it receives
|
|
52
59
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
60
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
61
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
62
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
63
|
+
));
|
|
53
64
|
// Default: generateAgentName returns "folder (pid)"
|
|
54
65
|
mockedGenerateAgentName.mockImplementation((cwd, pid) => {
|
|
55
66
|
const folder = path.basename(cwd) || 'unknown';
|
|
@@ -10,17 +10,22 @@ import { CodexAdapter } from '../../adapters/CodexAdapter.js';
|
|
|
10
10
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
11
11
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { AgentRegistry, type RegistryEntry } from '../../utils/AgentRegistry.js';
|
|
13
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
13
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
14
14
|
import { batchGetSessionFileBirthtimes } from '../../utils/session.js';
|
|
15
15
|
import type { SessionFile } from '../../utils/session.js';
|
|
16
16
|
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
17
17
|
import type { MatchResult } from '../../utils/matching.js';
|
|
18
18
|
import * as os from 'os';
|
|
19
19
|
|
|
20
|
-
vi.mock('../../utils/process.js', () =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal() as typeof import('../../utils/process.js');
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
listAgentProcesses: vi.fn(),
|
|
25
|
+
enrichProcesses: vi.fn(),
|
|
26
|
+
captureProcessSnapshot: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
24
29
|
|
|
25
30
|
vi.mock('../../utils/session.js', async () => {
|
|
26
31
|
const actual = await vi.importActual('../../utils/session') as typeof import('../../utils/session');
|
|
@@ -37,6 +42,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
37
42
|
|
|
38
43
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
39
44
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
45
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
40
46
|
const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as MockedFunction<typeof batchGetSessionFileBirthtimes>;
|
|
41
47
|
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
42
48
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
@@ -48,11 +54,16 @@ describe('CodexAdapter', () => {
|
|
|
48
54
|
adapter = new CodexAdapter();
|
|
49
55
|
mockedListAgentProcesses.mockReset();
|
|
50
56
|
mockedEnrichProcesses.mockReset();
|
|
57
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
51
58
|
mockedBatchGetSessionFileBirthtimes.mockReset();
|
|
52
59
|
mockedMatchProcessesToSessions.mockReset();
|
|
53
60
|
mockedGenerateAgentName.mockReset();
|
|
54
61
|
// Default: enrichProcesses returns what it receives
|
|
55
62
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
63
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
64
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
65
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
66
|
+
));
|
|
56
67
|
// Default: generateAgentName returns "folder (pid)"
|
|
57
68
|
mockedGenerateAgentName.mockImplementation((cwd, pid) => {
|
|
58
69
|
const folder = path.basename(cwd) || 'unknown';
|
|
@@ -10,7 +10,7 @@ import * as path from 'path';
|
|
|
10
10
|
import { CopilotAdapter } from '../../adapters/CopilotAdapter.js';
|
|
11
11
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
13
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
13
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
14
14
|
import { generateAgentName } from '../../utils/matching.js';
|
|
15
15
|
import { AgentRegistry } from '../../utils/AgentRegistry.js';
|
|
16
16
|
|
|
@@ -20,6 +20,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
|
20
20
|
...actual,
|
|
21
21
|
listAgentProcesses: vi.fn(),
|
|
22
22
|
enrichProcesses: vi.fn(),
|
|
23
|
+
captureProcessSnapshot: vi.fn(),
|
|
23
24
|
};
|
|
24
25
|
});
|
|
25
26
|
|
|
@@ -29,6 +30,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
29
30
|
|
|
30
31
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
31
32
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
33
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
32
34
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
33
35
|
|
|
34
36
|
describe('CopilotAdapter', () => {
|
|
@@ -45,8 +47,13 @@ describe('CopilotAdapter', () => {
|
|
|
45
47
|
|
|
46
48
|
mockedListAgentProcesses.mockReset();
|
|
47
49
|
mockedEnrichProcesses.mockReset();
|
|
50
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
48
51
|
mockedGenerateAgentName.mockReset();
|
|
49
52
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
53
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
54
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
55
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
56
|
+
));
|
|
50
57
|
mockedGenerateAgentName.mockImplementation((cwd, pid) => `${path.basename(cwd) || 'unknown'} (${pid})`);
|
|
51
58
|
});
|
|
52
59
|
|
|
@@ -11,7 +11,7 @@ import { GeminiCliAdapter } from '../../adapters/GeminiCliAdapter.js';
|
|
|
11
11
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
13
13
|
import { AgentRegistry, type RegistryEntry } from '../../utils/AgentRegistry.js';
|
|
14
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
14
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
15
15
|
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
16
16
|
import * as crypto from 'crypto';
|
|
17
17
|
|
|
@@ -21,6 +21,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
|
21
21
|
...actual,
|
|
22
22
|
listAgentProcesses: vi.fn(),
|
|
23
23
|
enrichProcesses: vi.fn(),
|
|
24
|
+
captureProcessSnapshot: vi.fn(),
|
|
24
25
|
};
|
|
25
26
|
});
|
|
26
27
|
|
|
@@ -31,6 +32,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
31
32
|
|
|
32
33
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
33
34
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
35
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
34
36
|
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
35
37
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
36
38
|
|
|
@@ -45,10 +47,15 @@ describe('GeminiCliAdapter', () => {
|
|
|
45
47
|
adapter = new GeminiCliAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
|
|
46
48
|
mockedListAgentProcesses.mockReset();
|
|
47
49
|
mockedEnrichProcesses.mockReset();
|
|
50
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
48
51
|
mockedMatchProcessesToSessions.mockReset();
|
|
49
52
|
mockedGenerateAgentName.mockReset();
|
|
50
53
|
|
|
51
54
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
55
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
56
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
57
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
58
|
+
));
|
|
52
59
|
mockedMatchProcessesToSessions.mockReturnValue([]);
|
|
53
60
|
mockedGenerateAgentName.mockImplementation((cwd: string, pid: number) => {
|
|
54
61
|
const folder = path.basename(cwd) || 'unknown';
|
|
@@ -110,6 +117,15 @@ describe('GeminiCliAdapter', () => {
|
|
|
110
117
|
tty: 'ttys006',
|
|
111
118
|
})).toBe(true);
|
|
112
119
|
});
|
|
120
|
+
|
|
121
|
+
it('should recognize Windows executable and entrypoint paths', () => {
|
|
122
|
+
expect(adapter.canHandle({
|
|
123
|
+
pid: 7,
|
|
124
|
+
command: 'C:\\tools\\node.exe C:\\lib\\gemini.js',
|
|
125
|
+
cwd: 'C:\\repo',
|
|
126
|
+
tty: '',
|
|
127
|
+
})).toBe(true);
|
|
128
|
+
});
|
|
113
129
|
});
|
|
114
130
|
|
|
115
131
|
describe('detectAgents', () => {
|
|
@@ -144,7 +160,7 @@ describe('GeminiCliAdapter', () => {
|
|
|
144
160
|
it('should return process-only agents when no session files exist for the process', async () => {
|
|
145
161
|
const proc: ProcessInfo = {
|
|
146
162
|
pid: 1234,
|
|
147
|
-
command: 'gemini',
|
|
163
|
+
command: 'node /usr/local/bin/gemini',
|
|
148
164
|
cwd: '/repo',
|
|
149
165
|
tty: 'ttys001',
|
|
150
166
|
startTime: new Date('2026-04-18T00:00:00Z'),
|
|
@@ -257,7 +273,7 @@ describe('GeminiCliAdapter', () => {
|
|
|
257
273
|
|
|
258
274
|
const proc: ProcessInfo = {
|
|
259
275
|
pid: 42,
|
|
260
|
-
command: 'gemini',
|
|
276
|
+
command: 'node /usr/local/bin/gemini',
|
|
261
277
|
cwd,
|
|
262
278
|
tty: 'ttys001',
|
|
263
279
|
startTime: new Date('2026-04-18T00:00:00Z'),
|
|
@@ -383,7 +399,7 @@ describe('GeminiCliAdapter', () => {
|
|
|
383
399
|
|
|
384
400
|
const proc: ProcessInfo = {
|
|
385
401
|
pid: 7,
|
|
386
|
-
command: 'gemini',
|
|
402
|
+
command: 'node /usr/local/bin/gemini',
|
|
387
403
|
cwd: procCwd,
|
|
388
404
|
tty: 'ttys001',
|
|
389
405
|
startTime: new Date(),
|
|
@@ -13,7 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import { GrokCliAdapter } from '../../adapters/GrokCliAdapter.js';
|
|
14
14
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
15
15
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
16
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
16
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
17
17
|
import { generateAgentName } from '../../utils/matching.js';
|
|
18
18
|
|
|
19
19
|
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
@@ -22,6 +22,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
|
22
22
|
...actual,
|
|
23
23
|
listAgentProcesses: vi.fn(),
|
|
24
24
|
enrichProcesses: vi.fn(),
|
|
25
|
+
captureProcessSnapshot: vi.fn(),
|
|
25
26
|
};
|
|
26
27
|
});
|
|
27
28
|
|
|
@@ -35,6 +36,7 @@ vi.mock('../../utils/matching.js', async (importOriginal) => {
|
|
|
35
36
|
|
|
36
37
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
37
38
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
39
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
38
40
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
39
41
|
|
|
40
42
|
const SESSION_ID = '019f16c3-5d5d-7dc3-85d1-bc629416ca2d';
|
|
@@ -64,9 +66,14 @@ describe('GrokCliAdapter', () => {
|
|
|
64
66
|
|
|
65
67
|
mockedListAgentProcesses.mockReset();
|
|
66
68
|
mockedEnrichProcesses.mockReset();
|
|
69
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
67
70
|
mockedGenerateAgentName.mockReset();
|
|
68
71
|
|
|
69
72
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
73
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
74
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
75
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
76
|
+
));
|
|
70
77
|
mockedGenerateAgentName.mockImplementation((c: string, pid: number) => `${path.basename(c) || 'unknown'}-${pid}`);
|
|
71
78
|
});
|
|
72
79
|
|
|
@@ -9,14 +9,19 @@ import * as path from 'path';
|
|
|
9
9
|
import { OpenCodeAdapter } from '../../adapters/OpenCodeAdapter.js';
|
|
10
10
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
11
11
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
12
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
12
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
13
13
|
import { generateAgentName } from '../../utils/matching.js';
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
|
|
16
|
-
vi.mock('../../utils/process.js', () =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal() as typeof import('../../utils/process.js');
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
listAgentProcesses: vi.fn(),
|
|
21
|
+
enrichProcesses: vi.fn(),
|
|
22
|
+
captureProcessSnapshot: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
20
25
|
|
|
21
26
|
vi.mock('../../utils/matching.js', () => ({
|
|
22
27
|
generateAgentName: vi.fn(),
|
|
@@ -25,6 +30,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
25
30
|
|
|
26
31
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
27
32
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
33
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
28
34
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
29
35
|
|
|
30
36
|
function makeDb(queries: {
|
|
@@ -104,9 +110,14 @@ describe('OpenCodeAdapter', () => {
|
|
|
104
110
|
|
|
105
111
|
mockedListAgentProcesses.mockReset();
|
|
106
112
|
mockedEnrichProcesses.mockReset();
|
|
113
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
107
114
|
mockedGenerateAgentName.mockReset();
|
|
108
115
|
|
|
109
116
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
117
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
118
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
119
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
120
|
+
));
|
|
110
121
|
mockedGenerateAgentName.mockImplementation((cwd, pid) => {
|
|
111
122
|
const folder = path.basename(cwd) || 'unknown';
|
|
112
123
|
return `${folder}-${pid}`;
|
|
@@ -11,13 +11,18 @@ import { PiAdapter } from '../../adapters/PiAdapter.js';
|
|
|
11
11
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
13
13
|
import { AgentRegistry } from '../../utils/AgentRegistry.js';
|
|
14
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
14
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
15
15
|
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
16
16
|
|
|
17
|
-
vi.mock('../../utils/process.js', () =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal() as typeof import('../../utils/process.js');
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
listAgentProcesses: vi.fn(),
|
|
22
|
+
enrichProcesses: vi.fn(),
|
|
23
|
+
captureProcessSnapshot: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
21
26
|
|
|
22
27
|
vi.mock('../../utils/matching.js', () => ({
|
|
23
28
|
matchProcessesToSessions: vi.fn(),
|
|
@@ -26,6 +31,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
26
31
|
|
|
27
32
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
28
33
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
34
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
29
35
|
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
30
36
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
31
37
|
|
|
@@ -43,10 +49,15 @@ describe('PiAdapter', () => {
|
|
|
43
49
|
adapter = new PiAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
|
|
44
50
|
mockedListAgentProcesses.mockReset();
|
|
45
51
|
mockedEnrichProcesses.mockReset();
|
|
52
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
46
53
|
mockedMatchProcessesToSessions.mockReset();
|
|
47
54
|
mockedGenerateAgentName.mockReset();
|
|
48
55
|
|
|
49
56
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
57
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
58
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
59
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
60
|
+
));
|
|
50
61
|
mockedMatchProcessesToSessions.mockReturnValue([]);
|
|
51
62
|
mockedGenerateAgentName.mockImplementation((cwd: string, pid: number) => {
|
|
52
63
|
const folder = path.basename(cwd) || 'unknown';
|
|
@@ -67,6 +78,18 @@ describe('PiAdapter', () => {
|
|
|
67
78
|
expect(adapter.canHandle({ pid: 2, command: '/usr/local/bin/PI --model x', cwd: '/repo', tty: 'ttys002' })).toBe(true);
|
|
68
79
|
expect(adapter.canHandle({ pid: 3, command: 'node /opt/pi/bin/pi.js', cwd: '/repo', tty: 'ttys003' })).toBe(true);
|
|
69
80
|
expect(adapter.canHandle({ pid: 4, command: 'node /repo/feature-pi-adapter/script.js', cwd: '/repo', tty: 'ttys004' })).toBe(false);
|
|
81
|
+
expect(adapter.canHandle({ pid: 5, command: 'C:\\tools\\node.exe C:\\lib\\pi.js', cwd: '/repo', tty: 'ttys005' })).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects foreign executables from a hand-built snapshot context', async () => {
|
|
85
|
+
const foreign: ProcessInfo = {
|
|
86
|
+
pid: 6,
|
|
87
|
+
command: 'claude --resume /Users/x/repos/pi/session.jsonl',
|
|
88
|
+
cwd: '/repo',
|
|
89
|
+
tty: 'ttys006',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
expect(await adapter.detectAgents({ processes: [foreign] })).toEqual([]);
|
|
70
93
|
});
|
|
71
94
|
|
|
72
95
|
it('maps a running Pi process to the tracker session for its PID', async () => {
|
|
@@ -29,6 +29,7 @@ describe('AgentRegistry', () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
afterEach(() => {
|
|
32
|
+
vi.restoreAllMocks();
|
|
32
33
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
33
34
|
});
|
|
34
35
|
|
|
@@ -81,6 +82,19 @@ describe('AgentRegistry', () => {
|
|
|
81
82
|
expect(registry.lookup(`ai-devkit-${process.pid}`)).toBeNull();
|
|
82
83
|
expect(registry.list()).toHaveLength(1);
|
|
83
84
|
});
|
|
85
|
+
|
|
86
|
+
it('preserves an existing name conflict when its probe fails with EPERM', () => {
|
|
87
|
+
registry.register(makeEntry({ name: 'claimed-name', pid: process.pid }));
|
|
88
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
89
|
+
throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(() => registry.register(makeEntry({
|
|
93
|
+
name: 'claimed-name',
|
|
94
|
+
pid: process.pid + 1,
|
|
95
|
+
}))).toThrow();
|
|
96
|
+
expect(registry.lookup('claimed-name')?.pid).toBe(process.pid);
|
|
97
|
+
});
|
|
84
98
|
});
|
|
85
99
|
|
|
86
100
|
describe('registerBatch', () => {
|
|
@@ -118,6 +132,33 @@ describe('AgentRegistry', () => {
|
|
|
118
132
|
expect(registry.list()).toHaveLength(1);
|
|
119
133
|
expect(registry.lookup('custom-name')?.pid).toBe(process.pid);
|
|
120
134
|
});
|
|
135
|
+
|
|
136
|
+
it('cleans up a cross-type row when its pid has been reused', () => {
|
|
137
|
+
registry.register(makeEntry({ name: 'old-claude', type: 'claude', pid: process.pid }));
|
|
138
|
+
|
|
139
|
+
registry.register(makeEntry({
|
|
140
|
+
name: 'new-codex',
|
|
141
|
+
type: 'codex',
|
|
142
|
+
pid: process.pid,
|
|
143
|
+
tmuxSession: '',
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
expect(registry.lookup('old-claude')).toBeNull();
|
|
147
|
+
expect(registry.lookup('new-codex')).toMatchObject({ type: 'codex', pid: process.pid });
|
|
148
|
+
expect(registry.list()).toHaveLength(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rolls back the whole batch when a live name conflict rejects one entry', () => {
|
|
152
|
+
registry.register(makeEntry({ name: 'taken', pid: process.pid }));
|
|
153
|
+
|
|
154
|
+
expect(() => registry.registerBatch([
|
|
155
|
+
makeEntry({ name: 'fresh', pid: 999998 }),
|
|
156
|
+
makeEntry({ name: 'taken', type: 'codex', pid: 999997 }),
|
|
157
|
+
])).toThrow(/UNIQUE constraint failed/);
|
|
158
|
+
|
|
159
|
+
expect(registry.lookup('fresh')).toBeNull();
|
|
160
|
+
expect(registry.lookup('taken')?.pid).toBe(process.pid);
|
|
161
|
+
});
|
|
121
162
|
});
|
|
122
163
|
|
|
123
164
|
describe('lookup', () => {
|
|
@@ -157,6 +198,30 @@ describe('AgentRegistry', () => {
|
|
|
157
198
|
it('returns false for a PID that does not exist', () => {
|
|
158
199
|
expect(registry.isAlive(makeEntry({ pid: 999999 }))).toBe(false);
|
|
159
200
|
});
|
|
201
|
+
|
|
202
|
+
it('returns true when the process probe is forbidden with EPERM', () => {
|
|
203
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
204
|
+
throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(registry.isAlive(makeEntry())).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('returns false when the process probe reports ESRCH', () => {
|
|
211
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
212
|
+
throw Object.assign(new Error('no such process'), { code: 'ESRCH' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(registry.isAlive(makeEntry())).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('returns true when the process probe fails without a definitive error code', () => {
|
|
219
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
220
|
+
throw new Error('indeterminate probe failure');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(registry.isAlive(makeEntry())).toBe(true);
|
|
224
|
+
});
|
|
160
225
|
});
|
|
161
226
|
|
|
162
227
|
describe('prune', () => {
|
|
@@ -177,9 +242,52 @@ describe('AgentRegistry', () => {
|
|
|
177
242
|
expect(after).toEqual(before);
|
|
178
243
|
});
|
|
179
244
|
|
|
245
|
+
it('preserves entries when liveness probing fails with EPERM', () => {
|
|
246
|
+
registry.register(makeEntry({ name: 'custom-name', tmuxSession: 'tmux-custom' }));
|
|
247
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
248
|
+
throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
registry.prune();
|
|
252
|
+
|
|
253
|
+
expect(registry.lookup('custom-name')).toMatchObject({
|
|
254
|
+
name: 'custom-name',
|
|
255
|
+
tmuxSession: 'tmux-custom',
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('removes entries when liveness probing fails with ESRCH', () => {
|
|
260
|
+
registry.register(makeEntry({ name: 'dead' }));
|
|
261
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
262
|
+
throw Object.assign(new Error('no such process'), { code: 'ESRCH' });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
registry.prune();
|
|
266
|
+
|
|
267
|
+
expect(registry.lookup('dead')).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
|
|
180
270
|
it('does nothing when file is missing', () => {
|
|
181
271
|
expect(() => registry.prune()).not.toThrow();
|
|
182
272
|
});
|
|
273
|
+
|
|
274
|
+
it('keeps forced prune available before the passive cadence is due', () => {
|
|
275
|
+
let nowMs = Date.parse('2026-08-14T10:00:00.000Z');
|
|
276
|
+
const clocked = new AgentRegistry(regPath, {
|
|
277
|
+
now: () => new Date(nowMs),
|
|
278
|
+
pruneIntervalMs: 30_000,
|
|
279
|
+
});
|
|
280
|
+
clocked.register(makeEntry({ name: 'forced', pid: process.pid }));
|
|
281
|
+
const alive = vi.spyOn(clocked, 'isAlive').mockReturnValue(true);
|
|
282
|
+
clocked.pruneIfDue();
|
|
283
|
+
alive.mockReturnValue(false);
|
|
284
|
+
nowMs += 1;
|
|
285
|
+
|
|
286
|
+
clocked.prune();
|
|
287
|
+
|
|
288
|
+
expect(alive).toHaveBeenCalledTimes(2);
|
|
289
|
+
expect(clocked.lookup('forced')).toBeNull();
|
|
290
|
+
});
|
|
183
291
|
});
|
|
184
292
|
|
|
185
293
|
describe('default()', () => {
|
|
@@ -215,6 +323,17 @@ describe('AgentRegistry', () => {
|
|
|
215
323
|
expect(() => registry.rename('agent-a', 'agent-b')).toThrow(RenameConflictError);
|
|
216
324
|
});
|
|
217
325
|
|
|
326
|
+
it('throws RenameConflictError when the conflicting entry probe fails with EPERM', () => {
|
|
327
|
+
registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
|
|
328
|
+
registry.register(makeEntry({ name: 'agent-b', pid: process.ppid }));
|
|
329
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
330
|
+
throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
expect(() => registry.rename('agent-a', 'agent-b')).toThrow(RenameConflictError);
|
|
334
|
+
expect(registry.lookup('agent-b')?.pid).toBe(process.ppid);
|
|
335
|
+
});
|
|
336
|
+
|
|
218
337
|
it('succeeds when new name exists only as a stale (dead) entry', () => {
|
|
219
338
|
registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
|
|
220
339
|
registry.register(makeEntry({ name: 'agent-b', pid: 999999 }));
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { MockedFunction } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { execFileSync } from 'child_process';
|
|
7
|
+
import { execFile, execFileSync } from 'child_process';
|
|
8
8
|
import {
|
|
9
9
|
listAgentProcesses,
|
|
10
10
|
batchGetProcessCwds,
|
|
@@ -12,13 +12,84 @@ import {
|
|
|
12
12
|
enrichProcesses,
|
|
13
13
|
findWrapperProcess,
|
|
14
14
|
findWrapperProcessPids,
|
|
15
|
+
captureProcessSnapshot,
|
|
16
|
+
filterByProcessNames,
|
|
15
17
|
} from '../../utils/process.js';
|
|
16
18
|
|
|
17
19
|
vi.mock('child_process', () => ({
|
|
20
|
+
execFile: vi.fn(),
|
|
18
21
|
execFileSync: vi.fn(),
|
|
19
22
|
}));
|
|
20
23
|
|
|
21
24
|
const mockedExecFileSync = execFileSync as MockedFunction<typeof execFileSync>;
|
|
25
|
+
const mockedExecFile = execFile as unknown as MockedFunction<(
|
|
26
|
+
file: string,
|
|
27
|
+
args: readonly string[],
|
|
28
|
+
options: object,
|
|
29
|
+
callback: (error: Error | null, stdout: string, stderr: string) => void,
|
|
30
|
+
) => void>;
|
|
31
|
+
|
|
32
|
+
describe('captureProcessSnapshot', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
mockedExecFile.mockReset();
|
|
35
|
+
mockedExecFileSync.mockReset();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('captures and enriches relevant processes without synchronous scans', async () => {
|
|
39
|
+
mockedExecFile.mockImplementation((file, args, _options, callback) => {
|
|
40
|
+
queueMicrotask(() => {
|
|
41
|
+
if (file === 'ps' && args.includes('-axo')) {
|
|
42
|
+
callback(null,
|
|
43
|
+
'100 1 s001 /usr/bin/claude --resume abc\n' +
|
|
44
|
+
'200 1 s002 C:\\\\tools\\\\node.exe C:\\\\bin\\\\pi.js\n' +
|
|
45
|
+
'300 1 s003 /usr/bin/unrelated\n',
|
|
46
|
+
'');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (file === 'lsof') {
|
|
50
|
+
callback(null, 'p100\nn/projects/claude\np200\nn/projects/pi\n', '');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (file === 'ps' && args.some((arg) => arg.includes('lstart='))) {
|
|
54
|
+
callback(null,
|
|
55
|
+
'100 Wed Mar 18 23:18:01 2026\n' +
|
|
56
|
+
'200 Thu Mar 19 10:00:00 2026\n',
|
|
57
|
+
'');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
callback(new Error(`unexpected command: ${file} ${args.join(' ')}`), '', '');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const snapshot = await captureProcessSnapshot(['claude', 'node']);
|
|
65
|
+
|
|
66
|
+
expect(snapshot.map((process) => process.pid)).toEqual([100, 200]);
|
|
67
|
+
expect(snapshot.map((process) => process.cwd)).toEqual(['/projects/claude', '/projects/pi']);
|
|
68
|
+
expect(snapshot.map((process) => process.startTime)).toEqual([
|
|
69
|
+
expect.any(Date),
|
|
70
|
+
expect.any(Date),
|
|
71
|
+
]);
|
|
72
|
+
expect(mockedExecFileSync).not.toHaveBeenCalled();
|
|
73
|
+
expect(mockedExecFile.mock.calls.filter(([, args]) => args.includes('-axo'))).toHaveLength(1);
|
|
74
|
+
for (const [, , options] of mockedExecFile.mock.calls) {
|
|
75
|
+
expect(options).toMatchObject({ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
76
|
+
expect(options).not.toHaveProperty('stdio');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('filterByProcessNames', () => {
|
|
82
|
+
it('matches only argv[0] and normalizes Windows paths and executable suffixes', () => {
|
|
83
|
+
const processes: ProcessInfo[] = [
|
|
84
|
+
{ pid: 1, command: 'C:\\tools\\node.exe C:\\bin\\pi.js', cwd: '', tty: '' },
|
|
85
|
+
{ pid: 2, command: '/usr/local/bin/node /opt/gemini.js', cwd: '', tty: '' },
|
|
86
|
+
{ pid: 3, command: 'codex exec --cd C:\\repos\\node', cwd: '', tty: '' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
expect(filterByProcessNames(processes, ['node'])).toEqual([processes[0], processes[1]]);
|
|
90
|
+
expect(filterByProcessNames(processes, ['node.exe'])).toEqual([processes[0], processes[1]]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
22
93
|
|
|
23
94
|
describe('listAgentProcesses', () => {
|
|
24
95
|
beforeEach(() => {
|