@ai-devkit/agent-manager 0.15.0 → 0.17.0

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.
Files changed (112) hide show
  1. package/.swcrc +27 -0
  2. package/dist/AgentManager.d.ts +4 -0
  3. package/dist/AgentManager.d.ts.map +1 -1
  4. package/dist/AgentManager.js +97 -64
  5. package/dist/AgentManager.js.map +1 -1
  6. package/dist/__tests__/AgentManager.test.js +564 -0
  7. package/dist/__tests__/AgentManager.test.js.map +1 -0
  8. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +2157 -0
  9. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -0
  10. package/dist/__tests__/adapters/CodexAdapter.test.js +1354 -0
  11. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -0
  12. package/dist/__tests__/adapters/CopilotAdapter.test.js +500 -0
  13. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -0
  14. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +1229 -0
  15. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -0
  16. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +626 -0
  17. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -0
  18. package/dist/__tests__/terminal/TmuxManager.test.js +159 -0
  19. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -0
  20. package/dist/__tests__/terminal/TtyWriter.test.js +162 -0
  21. package/dist/__tests__/terminal/TtyWriter.test.js.map +1 -0
  22. package/dist/__tests__/utils/AgentRegistry.test.js +282 -0
  23. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -0
  24. package/dist/__tests__/utils/ClaudeSessionParser.test.js +312 -0
  25. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -0
  26. package/dist/__tests__/utils/matching.test.js +324 -0
  27. package/dist/__tests__/utils/matching.test.js.map +1 -0
  28. package/dist/__tests__/utils/process.test.js +177 -0
  29. package/dist/__tests__/utils/process.test.js.map +1 -0
  30. package/dist/__tests__/utils/session.test.js +158 -0
  31. package/dist/__tests__/utils/session.test.js.map +1 -0
  32. package/dist/adapters/AgentAdapter.d.ts +1 -1
  33. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  34. package/dist/adapters/AgentAdapter.js +8 -7
  35. package/dist/adapters/AgentAdapter.js.map +1 -1
  36. package/dist/adapters/ClaudeCodeAdapter.js +66 -77
  37. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  38. package/dist/adapters/CodexAdapter.d.ts +13 -1
  39. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  40. package/dist/adapters/CodexAdapter.js +208 -132
  41. package/dist/adapters/CodexAdapter.js.map +1 -1
  42. package/dist/adapters/CopilotAdapter.d.ts +44 -0
  43. package/dist/adapters/CopilotAdapter.d.ts.map +1 -0
  44. package/dist/adapters/CopilotAdapter.js +314 -0
  45. package/dist/adapters/CopilotAdapter.js.map +1 -0
  46. package/dist/adapters/GeminiCliAdapter.d.ts +6 -1
  47. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  48. package/dist/adapters/GeminiCliAdapter.js +160 -149
  49. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  50. package/dist/adapters/OpenCodeAdapter.js +60 -69
  51. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  52. package/dist/adapters/index.d.ts +1 -0
  53. package/dist/adapters/index.d.ts.map +1 -1
  54. package/dist/adapters/index.js +2 -0
  55. package/dist/adapters/index.js.map +1 -1
  56. package/dist/index.d.ts +6 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +5 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/terminal/TerminalFocusManager.js +62 -49
  61. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  62. package/dist/terminal/TmuxManager.d.ts +28 -0
  63. package/dist/terminal/TmuxManager.d.ts.map +1 -0
  64. package/dist/terminal/TmuxManager.js +137 -0
  65. package/dist/terminal/TmuxManager.js.map +1 -0
  66. package/dist/terminal/TtyWriter.js +37 -16
  67. package/dist/terminal/TtyWriter.js.map +1 -1
  68. package/dist/terminal/index.js +1 -0
  69. package/dist/terminal/index.js.map +1 -1
  70. package/dist/utils/AgentRegistry.d.ts +35 -0
  71. package/dist/utils/AgentRegistry.d.ts.map +1 -0
  72. package/dist/utils/AgentRegistry.js +123 -0
  73. package/dist/utils/AgentRegistry.js.map +1 -0
  74. package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
  75. package/dist/utils/ClaudeSessionParser.js +62 -93
  76. package/dist/utils/ClaudeSessionParser.js.map +1 -1
  77. package/dist/utils/agents.d.ts +15 -0
  78. package/dist/utils/agents.d.ts.map +1 -0
  79. package/dist/utils/agents.js +41 -0
  80. package/dist/utils/agents.js.map +1 -0
  81. package/dist/utils/applescript.js +3 -6
  82. package/dist/utils/applescript.js.map +1 -1
  83. package/dist/utils/index.js +1 -0
  84. package/dist/utils/index.js.map +1 -1
  85. package/dist/utils/matching.js +21 -29
  86. package/dist/utils/matching.js.map +1 -1
  87. package/dist/utils/process.js +77 -56
  88. package/dist/utils/process.js.map +1 -1
  89. package/dist/utils/session.js +20 -34
  90. package/dist/utils/session.js.map +1 -1
  91. package/dist/utils/sortAgents.js +6 -8
  92. package/dist/utils/sortAgents.js.map +1 -1
  93. package/package.json +11 -7
  94. package/src/AgentManager.ts +40 -0
  95. package/src/__tests__/AgentManager.test.ts +135 -0
  96. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +3 -2
  97. package/src/__tests__/adapters/CodexAdapter.test.ts +199 -0
  98. package/src/__tests__/adapters/CopilotAdapter.test.ts +411 -0
  99. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +185 -1
  100. package/src/__tests__/terminal/TmuxManager.test.ts +175 -0
  101. package/src/__tests__/utils/AgentRegistry.test.ts +223 -0
  102. package/src/__tests__/utils/ClaudeSessionParser.test.ts +71 -0
  103. package/src/adapters/AgentAdapter.ts +1 -1
  104. package/src/adapters/CodexAdapter.ts +155 -8
  105. package/src/adapters/CopilotAdapter.ts +453 -0
  106. package/src/adapters/GeminiCliAdapter.ts +84 -6
  107. package/src/adapters/index.ts +1 -0
  108. package/src/index.ts +7 -0
  109. package/src/terminal/TmuxManager.ts +118 -0
  110. package/src/utils/AgentRegistry.ts +139 -0
  111. package/src/utils/ClaudeSessionParser.ts +21 -10
  112. package/src/utils/agents.ts +41 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Tests for utils/matching.ts
3
+ */ import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
4
+ function makeProcess(overrides) {
5
+ return {
6
+ command: 'claude',
7
+ cwd: '/projects/my-app',
8
+ tty: 'ttys001',
9
+ startTime: new Date('2026-03-18T23:18:01.000Z'),
10
+ ...overrides
11
+ };
12
+ }
13
+ function makeSession(overrides) {
14
+ return {
15
+ filePath: `/home/.claude/projects/my-app/${overrides.sessionId}.jsonl`,
16
+ projectDir: '/home/.claude/projects/my-app',
17
+ birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime(),
18
+ resolvedCwd: '/projects/my-app',
19
+ ...overrides
20
+ };
21
+ }
22
+ describe('matchProcessesToSessions', ()=>{
23
+ it('should return empty array when no processes', ()=>{
24
+ const sessions = [
25
+ makeSession({
26
+ sessionId: 's1'
27
+ })
28
+ ];
29
+ expect(matchProcessesToSessions([], sessions)).toEqual([]);
30
+ });
31
+ it('should return empty array when no sessions', ()=>{
32
+ const processes = [
33
+ makeProcess({
34
+ pid: 100
35
+ })
36
+ ];
37
+ expect(matchProcessesToSessions(processes, [])).toEqual([]);
38
+ });
39
+ it('should match a single process to closest session', ()=>{
40
+ const proc = makeProcess({
41
+ pid: 100,
42
+ startTime: new Date('2026-03-18T23:18:01.000Z')
43
+ });
44
+ const s1 = makeSession({
45
+ sessionId: 's1',
46
+ birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime()
47
+ });
48
+ const s2 = makeSession({
49
+ sessionId: 's2',
50
+ birthtimeMs: new Date('2026-03-18T23:20:00.000Z').getTime()
51
+ });
52
+ const results = matchProcessesToSessions([
53
+ proc
54
+ ], [
55
+ s1,
56
+ s2
57
+ ]);
58
+ expect(results).toHaveLength(1);
59
+ expect(results[0].session.sessionId).toBe('s1');
60
+ expect(results[0].deltaMs).toBe(43000);
61
+ });
62
+ it('should enforce 1:1 constraint — each process matches only one session', ()=>{
63
+ const p1 = makeProcess({
64
+ pid: 100,
65
+ startTime: new Date('2026-03-18T23:18:01.000Z')
66
+ });
67
+ const p2 = makeProcess({
68
+ pid: 200,
69
+ startTime: new Date('2026-03-18T23:18:30.000Z')
70
+ });
71
+ const s1 = makeSession({
72
+ sessionId: 's1',
73
+ birthtimeMs: new Date('2026-03-18T23:18:10.000Z').getTime()
74
+ });
75
+ const s2 = makeSession({
76
+ sessionId: 's2',
77
+ birthtimeMs: new Date('2026-03-18T23:18:35.000Z').getTime()
78
+ });
79
+ const results = matchProcessesToSessions([
80
+ p1,
81
+ p2
82
+ ], [
83
+ s1,
84
+ s2
85
+ ]);
86
+ expect(results).toHaveLength(2);
87
+ const pids = results.map((r)=>r.process.pid).sort();
88
+ const sids = results.map((r)=>r.session.sessionId).sort();
89
+ expect(pids).toEqual([
90
+ 100,
91
+ 200
92
+ ]);
93
+ expect(sids).toEqual([
94
+ 's1',
95
+ 's2'
96
+ ]);
97
+ });
98
+ it('should disambiguate multiple processes with same CWD by birthtime', ()=>{
99
+ const p1 = makeProcess({
100
+ pid: 100,
101
+ startTime: new Date('2026-03-18T23:18:01.000Z')
102
+ });
103
+ const p2 = makeProcess({
104
+ pid: 200,
105
+ startTime: new Date('2026-03-19T08:53:11.000Z')
106
+ });
107
+ const s1 = makeSession({
108
+ sessionId: 's1',
109
+ birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime()
110
+ });
111
+ const s2 = makeSession({
112
+ sessionId: 's2',
113
+ birthtimeMs: new Date('2026-03-19T08:55:35.000Z').getTime()
114
+ });
115
+ const results = matchProcessesToSessions([
116
+ p1,
117
+ p2
118
+ ], [
119
+ s1,
120
+ s2
121
+ ]);
122
+ expect(results).toHaveLength(2);
123
+ const match1 = results.find((r)=>r.process.pid === 100);
124
+ const match2 = results.find((r)=>r.process.pid === 200);
125
+ expect(match1?.session.sessionId).toBe('s1');
126
+ expect(match2?.session.sessionId).toBe('s2');
127
+ });
128
+ it('should exclude processes without startTime', ()=>{
129
+ const proc = makeProcess({
130
+ pid: 100,
131
+ startTime: undefined
132
+ });
133
+ const session = makeSession({
134
+ sessionId: 's1'
135
+ });
136
+ const results = matchProcessesToSessions([
137
+ proc
138
+ ], [
139
+ session
140
+ ]);
141
+ expect(results).toEqual([]);
142
+ });
143
+ it('should exclude processes without cwd', ()=>{
144
+ const proc = makeProcess({
145
+ pid: 100,
146
+ cwd: ''
147
+ });
148
+ const session = makeSession({
149
+ sessionId: 's1'
150
+ });
151
+ const results = matchProcessesToSessions([
152
+ proc
153
+ ], [
154
+ session
155
+ ]);
156
+ expect(results).toEqual([]);
157
+ });
158
+ it('should not match when CWD does not match resolvedCwd', ()=>{
159
+ const proc = makeProcess({
160
+ pid: 100,
161
+ cwd: '/projects/app-a'
162
+ });
163
+ const session = makeSession({
164
+ sessionId: 's1',
165
+ resolvedCwd: '/projects/app-b'
166
+ });
167
+ const results = matchProcessesToSessions([
168
+ proc
169
+ ], [
170
+ session
171
+ ]);
172
+ expect(results).toEqual([]);
173
+ });
174
+ it('should not match when delta exceeds 3-minute tolerance', ()=>{
175
+ const proc = makeProcess({
176
+ pid: 100,
177
+ startTime: new Date('2026-03-18T23:18:01.000Z')
178
+ });
179
+ const session = makeSession({
180
+ sessionId: 's1',
181
+ birthtimeMs: new Date('2026-03-18T23:22:00.000Z').getTime()
182
+ });
183
+ const results = matchProcessesToSessions([
184
+ proc
185
+ ], [
186
+ session
187
+ ]);
188
+ expect(results).toEqual([]);
189
+ });
190
+ it('should match at exactly 3-minute boundary', ()=>{
191
+ const startTime = new Date('2026-03-18T23:18:00.000Z');
192
+ const proc = makeProcess({
193
+ pid: 100,
194
+ startTime
195
+ });
196
+ const session = makeSession({
197
+ sessionId: 's1',
198
+ birthtimeMs: startTime.getTime() + 180_000
199
+ });
200
+ const results = matchProcessesToSessions([
201
+ proc
202
+ ], [
203
+ session
204
+ ]);
205
+ expect(results).toHaveLength(1);
206
+ });
207
+ it('should handle more sessions than processes', ()=>{
208
+ const proc = makeProcess({
209
+ pid: 100,
210
+ startTime: new Date('2026-03-18T23:18:01.000Z')
211
+ });
212
+ const s1 = makeSession({
213
+ sessionId: 's1',
214
+ birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime()
215
+ });
216
+ const s2 = makeSession({
217
+ sessionId: 's2',
218
+ birthtimeMs: new Date('2026-03-18T23:19:00.000Z').getTime()
219
+ });
220
+ const s3 = makeSession({
221
+ sessionId: 's3',
222
+ birthtimeMs: new Date('2026-03-18T23:19:30.000Z').getTime()
223
+ });
224
+ const results = matchProcessesToSessions([
225
+ proc
226
+ ], [
227
+ s1,
228
+ s2,
229
+ s3
230
+ ]);
231
+ expect(results).toHaveLength(1);
232
+ expect(results[0].session.sessionId).toBe('s1'); // closest
233
+ });
234
+ it('should handle more processes than sessions', ()=>{
235
+ const p1 = makeProcess({
236
+ pid: 100,
237
+ startTime: new Date('2026-03-18T23:18:01.000Z')
238
+ });
239
+ const p2 = makeProcess({
240
+ pid: 200,
241
+ startTime: new Date('2026-03-18T23:20:01.000Z')
242
+ });
243
+ const s1 = makeSession({
244
+ sessionId: 's1',
245
+ birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime()
246
+ });
247
+ const results = matchProcessesToSessions([
248
+ p1,
249
+ p2
250
+ ], [
251
+ s1
252
+ ]);
253
+ expect(results).toHaveLength(1);
254
+ expect(results[0].process.pid).toBe(100); // closest
255
+ });
256
+ it('should skip sessions with empty resolvedCwd', ()=>{
257
+ const proc = makeProcess({
258
+ pid: 100
259
+ });
260
+ const session = makeSession({
261
+ sessionId: 's1',
262
+ resolvedCwd: ''
263
+ });
264
+ const results = matchProcessesToSessions([
265
+ proc
266
+ ], [
267
+ session
268
+ ]);
269
+ expect(results).toEqual([]);
270
+ });
271
+ it('should prefer best match when greedy ordering matters', ()=>{
272
+ // p1 is 10s from s2, p2 is 5s from s2 — p2 should win s2, p1 gets s1
273
+ const p1 = makeProcess({
274
+ pid: 100,
275
+ startTime: new Date('2026-03-18T23:18:00.000Z')
276
+ });
277
+ const p2 = makeProcess({
278
+ pid: 200,
279
+ startTime: new Date('2026-03-18T23:18:25.000Z')
280
+ });
281
+ const s1 = makeSession({
282
+ sessionId: 's1',
283
+ birthtimeMs: new Date('2026-03-18T23:18:08.000Z').getTime()
284
+ });
285
+ const s2 = makeSession({
286
+ sessionId: 's2',
287
+ birthtimeMs: new Date('2026-03-18T23:18:30.000Z').getTime()
288
+ });
289
+ const results = matchProcessesToSessions([
290
+ p1,
291
+ p2
292
+ ], [
293
+ s1,
294
+ s2
295
+ ]);
296
+ expect(results).toHaveLength(2);
297
+ const match1 = results.find((r)=>r.process.pid === 200);
298
+ const match2 = results.find((r)=>r.process.pid === 100);
299
+ expect(match1?.session.sessionId).toBe('s2'); // 5s delta
300
+ expect(match2?.session.sessionId).toBe('s1'); // 8s delta
301
+ });
302
+ });
303
+ describe('generateAgentName', ()=>{
304
+ it('should return lowercase kebab-case name with pid', ()=>{
305
+ expect(generateAgentName('/projects/my-app', 12345)).toBe('my-app-12345');
306
+ });
307
+ it('should handle root path', ()=>{
308
+ expect(generateAgentName('/', 100)).toBe('unknown-100');
309
+ });
310
+ it('should handle empty cwd', ()=>{
311
+ expect(generateAgentName('', 100)).toBe('unknown-100');
312
+ });
313
+ it('should handle nested paths', ()=>{
314
+ expect(generateAgentName('/home/user/projects/ai-devkit', 78070)).toBe('ai-devkit-78070');
315
+ });
316
+ it('should convert spaces and special chars to kebab-case', ()=>{
317
+ expect(generateAgentName('/projects/AI DevKit', 123)).toBe('ai-devkit-123');
318
+ });
319
+ it('should convert uppercase to lowercase', ()=>{
320
+ expect(generateAgentName('/projects/MyProject', 456)).toBe('myproject-456');
321
+ });
322
+ });
323
+
324
+ //# sourceMappingURL=matching.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/utils/matching.test.ts"],"sourcesContent":["/**\n * Tests for utils/matching.ts\n */\n\n\nimport { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';\nimport type { ProcessInfo } from '../../adapters/AgentAdapter.js';\nimport type { SessionFile } from '../../utils/session.js';\n\nfunction makeProcess(overrides: Partial<ProcessInfo> & { pid: number }): ProcessInfo {\n return {\n command: 'claude',\n cwd: '/projects/my-app',\n tty: 'ttys001',\n startTime: new Date('2026-03-18T23:18:01.000Z'),\n ...overrides,\n };\n}\n\nfunction makeSession(overrides: Partial<SessionFile> & { sessionId: string }): SessionFile {\n return {\n filePath: `/home/.claude/projects/my-app/${overrides.sessionId}.jsonl`,\n projectDir: '/home/.claude/projects/my-app',\n birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime(),\n resolvedCwd: '/projects/my-app',\n ...overrides,\n };\n}\n\ndescribe('matchProcessesToSessions', () => {\n it('should return empty array when no processes', () => {\n const sessions = [makeSession({ sessionId: 's1' })];\n expect(matchProcessesToSessions([], sessions)).toEqual([]);\n });\n\n it('should return empty array when no sessions', () => {\n const processes = [makeProcess({ pid: 100 })];\n expect(matchProcessesToSessions(processes, [])).toEqual([]);\n });\n\n it('should match a single process to closest session', () => {\n const proc = makeProcess({ pid: 100, startTime: new Date('2026-03-18T23:18:01.000Z') });\n const s1 = makeSession({ sessionId: 's1', birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime() });\n const s2 = makeSession({ sessionId: 's2', birthtimeMs: new Date('2026-03-18T23:20:00.000Z').getTime() });\n\n const results = matchProcessesToSessions([proc], [s1, s2]);\n expect(results).toHaveLength(1);\n expect(results[0].session.sessionId).toBe('s1');\n expect(results[0].deltaMs).toBe(43000);\n });\n\n it('should enforce 1:1 constraint — each process matches only one session', () => {\n const p1 = makeProcess({ pid: 100, startTime: new Date('2026-03-18T23:18:01.000Z') });\n const p2 = makeProcess({ pid: 200, startTime: new Date('2026-03-18T23:18:30.000Z') });\n const s1 = makeSession({ sessionId: 's1', birthtimeMs: new Date('2026-03-18T23:18:10.000Z').getTime() });\n const s2 = makeSession({ sessionId: 's2', birthtimeMs: new Date('2026-03-18T23:18:35.000Z').getTime() });\n\n const results = matchProcessesToSessions([p1, p2], [s1, s2]);\n expect(results).toHaveLength(2);\n\n const pids = results.map(r => r.process.pid).sort();\n const sids = results.map(r => r.session.sessionId).sort();\n expect(pids).toEqual([100, 200]);\n expect(sids).toEqual(['s1', 's2']);\n });\n\n it('should disambiguate multiple processes with same CWD by birthtime', () => {\n const p1 = makeProcess({ pid: 100, startTime: new Date('2026-03-18T23:18:01.000Z') });\n const p2 = makeProcess({ pid: 200, startTime: new Date('2026-03-19T08:53:11.000Z') });\n const s1 = makeSession({ sessionId: 's1', birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime() });\n const s2 = makeSession({ sessionId: 's2', birthtimeMs: new Date('2026-03-19T08:55:35.000Z').getTime() });\n\n const results = matchProcessesToSessions([p1, p2], [s1, s2]);\n expect(results).toHaveLength(2);\n\n const match1 = results.find(r => r.process.pid === 100);\n const match2 = results.find(r => r.process.pid === 200);\n expect(match1?.session.sessionId).toBe('s1');\n expect(match2?.session.sessionId).toBe('s2');\n });\n\n it('should exclude processes without startTime', () => {\n const proc = makeProcess({ pid: 100, startTime: undefined });\n const session = makeSession({ sessionId: 's1' });\n\n const results = matchProcessesToSessions([proc], [session]);\n expect(results).toEqual([]);\n });\n\n it('should exclude processes without cwd', () => {\n const proc = makeProcess({ pid: 100, cwd: '' });\n const session = makeSession({ sessionId: 's1' });\n\n const results = matchProcessesToSessions([proc], [session]);\n expect(results).toEqual([]);\n });\n\n it('should not match when CWD does not match resolvedCwd', () => {\n const proc = makeProcess({ pid: 100, cwd: '/projects/app-a' });\n const session = makeSession({ sessionId: 's1', resolvedCwd: '/projects/app-b' });\n\n const results = matchProcessesToSessions([proc], [session]);\n expect(results).toEqual([]);\n });\n\n it('should not match when delta exceeds 3-minute tolerance', () => {\n const proc = makeProcess({ pid: 100, startTime: new Date('2026-03-18T23:18:01.000Z') });\n const session = makeSession({\n sessionId: 's1',\n birthtimeMs: new Date('2026-03-18T23:22:00.000Z').getTime(), // ~4 min later\n });\n\n const results = matchProcessesToSessions([proc], [session]);\n expect(results).toEqual([]);\n });\n\n it('should match at exactly 3-minute boundary', () => {\n const startTime = new Date('2026-03-18T23:18:00.000Z');\n const proc = makeProcess({ pid: 100, startTime });\n const session = makeSession({\n sessionId: 's1',\n birthtimeMs: startTime.getTime() + 180_000, // exactly 3 min\n });\n\n const results = matchProcessesToSessions([proc], [session]);\n expect(results).toHaveLength(1);\n });\n\n it('should handle more sessions than processes', () => {\n const proc = makeProcess({ pid: 100, startTime: new Date('2026-03-18T23:18:01.000Z') });\n const s1 = makeSession({ sessionId: 's1', birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime() });\n const s2 = makeSession({ sessionId: 's2', birthtimeMs: new Date('2026-03-18T23:19:00.000Z').getTime() });\n const s3 = makeSession({ sessionId: 's3', birthtimeMs: new Date('2026-03-18T23:19:30.000Z').getTime() });\n\n const results = matchProcessesToSessions([proc], [s1, s2, s3]);\n expect(results).toHaveLength(1);\n expect(results[0].session.sessionId).toBe('s1'); // closest\n });\n\n it('should handle more processes than sessions', () => {\n const p1 = makeProcess({ pid: 100, startTime: new Date('2026-03-18T23:18:01.000Z') });\n const p2 = makeProcess({ pid: 200, startTime: new Date('2026-03-18T23:20:01.000Z') });\n const s1 = makeSession({ sessionId: 's1', birthtimeMs: new Date('2026-03-18T23:18:44.000Z').getTime() });\n\n const results = matchProcessesToSessions([p1, p2], [s1]);\n expect(results).toHaveLength(1);\n expect(results[0].process.pid).toBe(100); // closest\n });\n\n it('should skip sessions with empty resolvedCwd', () => {\n const proc = makeProcess({ pid: 100 });\n const session = makeSession({ sessionId: 's1', resolvedCwd: '' });\n\n const results = matchProcessesToSessions([proc], [session]);\n expect(results).toEqual([]);\n });\n\n it('should prefer best match when greedy ordering matters', () => {\n // p1 is 10s from s2, p2 is 5s from s2 — p2 should win s2, p1 gets s1\n const p1 = makeProcess({ pid: 100, startTime: new Date('2026-03-18T23:18:00.000Z') });\n const p2 = makeProcess({ pid: 200, startTime: new Date('2026-03-18T23:18:25.000Z') });\n const s1 = makeSession({ sessionId: 's1', birthtimeMs: new Date('2026-03-18T23:18:08.000Z').getTime() });\n const s2 = makeSession({ sessionId: 's2', birthtimeMs: new Date('2026-03-18T23:18:30.000Z').getTime() });\n\n const results = matchProcessesToSessions([p1, p2], [s1, s2]);\n expect(results).toHaveLength(2);\n\n const match1 = results.find(r => r.process.pid === 200);\n const match2 = results.find(r => r.process.pid === 100);\n expect(match1?.session.sessionId).toBe('s2'); // 5s delta\n expect(match2?.session.sessionId).toBe('s1'); // 8s delta\n });\n});\n\ndescribe('generateAgentName', () => {\n it('should return lowercase kebab-case name with pid', () => {\n expect(generateAgentName('/projects/my-app', 12345)).toBe('my-app-12345');\n });\n\n it('should handle root path', () => {\n expect(generateAgentName('/', 100)).toBe('unknown-100');\n });\n\n it('should handle empty cwd', () => {\n expect(generateAgentName('', 100)).toBe('unknown-100');\n });\n\n it('should handle nested paths', () => {\n expect(generateAgentName('/home/user/projects/ai-devkit', 78070)).toBe('ai-devkit-78070');\n });\n\n it('should convert spaces and special chars to kebab-case', () => {\n expect(generateAgentName('/projects/AI DevKit', 123)).toBe('ai-devkit-123');\n });\n\n it('should convert uppercase to lowercase', () => {\n expect(generateAgentName('/projects/MyProject', 456)).toBe('myproject-456');\n });\n});\n"],"names":["matchProcessesToSessions","generateAgentName","makeProcess","overrides","command","cwd","tty","startTime","Date","makeSession","filePath","sessionId","projectDir","birthtimeMs","getTime","resolvedCwd","describe","it","sessions","expect","toEqual","processes","pid","proc","s1","s2","results","toHaveLength","session","toBe","deltaMs","p1","p2","pids","map","r","process","sort","sids","match1","find","match2","undefined","s3"],"mappings":"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"}
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Tests for new functions in utils/process.ts
3
+ */ import { execFileSync } from 'child_process';
4
+ import { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses } from '../../utils/process.js';
5
+ vi.mock('child_process', ()=>({
6
+ execFileSync: vi.fn()
7
+ }));
8
+ const mockedExecFileSync = execFileSync;
9
+ describe('listAgentProcesses', ()=>{
10
+ beforeEach(()=>{
11
+ mockedExecFileSync.mockReset();
12
+ });
13
+ it('should parse ps aux | grep output and post-filter by executable name', ()=>{
14
+ mockedExecFileSync.mockReturnValue('user 78070 1.0 0.5 485636016 245952 s018 S+ 11:18PM 1:55.14 claude\n' + 'user 55106 0.1 0.4 485620368 72496 s015 S+ 9Mar26 8:06.36 claude\n');
15
+ const processes = listAgentProcesses('claude');
16
+ expect(processes).toHaveLength(2);
17
+ expect(processes[0].pid).toBe(78070);
18
+ expect(processes[0].command).toBe('claude');
19
+ expect(processes[0].tty).toBe('s018');
20
+ expect(processes[0].cwd).toBe(''); // not populated yet
21
+ expect(processes[1].pid).toBe(55106);
22
+ });
23
+ it('should filter out non-matching executables', ()=>{
24
+ mockedExecFileSync.mockReturnValue('user 100 0.0 0.0 0 0 s001 S 1:00PM 0:00 claude\n' + 'user 200 0.0 0.0 0 0 s002 S 1:00PM 0:00 claude-helper --pid 100\n' + 'user 300 0.0 0.0 0 0 s003 S 1:00PM 0:00 /usr/bin/claude\n');
25
+ const processes = listAgentProcesses('claude');
26
+ expect(processes).toHaveLength(2);
27
+ expect(processes.map((p)=>p.pid)).toEqual([
28
+ 100,
29
+ 300
30
+ ]);
31
+ });
32
+ it('should return empty array on command failure', ()=>{
33
+ mockedExecFileSync.mockImplementation(()=>{
34
+ throw new Error('fail');
35
+ });
36
+ expect(listAgentProcesses('claude')).toEqual([]);
37
+ });
38
+ it('should handle empty output', ()=>{
39
+ mockedExecFileSync.mockReturnValue('');
40
+ expect(listAgentProcesses('claude')).toEqual([]);
41
+ });
42
+ it('should reject empty pattern', ()=>{
43
+ expect(listAgentProcesses('')).toEqual([]);
44
+ expect(mockedExecFileSync).not.toHaveBeenCalled();
45
+ });
46
+ it('should reject patterns with shell injection characters', ()=>{
47
+ expect(listAgentProcesses('claude; rm -rf /')).toEqual([]);
48
+ expect(listAgentProcesses("claude' || true")).toEqual([]);
49
+ expect(listAgentProcesses('$(whoami)')).toEqual([]);
50
+ expect(mockedExecFileSync).not.toHaveBeenCalled();
51
+ });
52
+ it('should accept valid patterns with dashes and underscores', ()=>{
53
+ mockedExecFileSync.mockReturnValue('');
54
+ listAgentProcesses('claude-code');
55
+ expect(mockedExecFileSync).toHaveBeenCalled();
56
+ mockedExecFileSync.mockReset();
57
+ mockedExecFileSync.mockReturnValue('');
58
+ listAgentProcesses('my_agent');
59
+ expect(mockedExecFileSync).toHaveBeenCalled();
60
+ });
61
+ });
62
+ describe('batchGetProcessCwds', ()=>{
63
+ beforeEach(()=>{
64
+ mockedExecFileSync.mockReset();
65
+ });
66
+ it('should parse batched lsof output', ()=>{
67
+ mockedExecFileSync.mockReturnValue('p78070\nn/Users/user/ai-devkit\np55106\nn/Users/user/other-project\n');
68
+ const cwds = batchGetProcessCwds([
69
+ 78070,
70
+ 55106
71
+ ]);
72
+ expect(cwds.get(78070)).toBe('/Users/user/ai-devkit');
73
+ expect(cwds.get(55106)).toBe('/Users/user/other-project');
74
+ });
75
+ it('should return empty map for empty pids', ()=>{
76
+ expect(batchGetProcessCwds([])).toEqual(new Map());
77
+ });
78
+ it('should return partial results when lsof succeeds for some PIDs', ()=>{
79
+ // lsof might not return entries for dead processes
80
+ mockedExecFileSync.mockReturnValue('p78070\nn/Users/user/ai-devkit\n');
81
+ const cwds = batchGetProcessCwds([
82
+ 78070,
83
+ 99999
84
+ ]);
85
+ expect(cwds.size).toBe(1);
86
+ expect(cwds.get(78070)).toBe('/Users/user/ai-devkit');
87
+ });
88
+ it('should return empty map on total failure', ()=>{
89
+ mockedExecFileSync.mockImplementation(()=>{
90
+ throw new Error('fail');
91
+ });
92
+ const cwds = batchGetProcessCwds([
93
+ 78070
94
+ ]);
95
+ // Falls through to pwdx fallback which also fails
96
+ expect(cwds.size).toBe(0);
97
+ });
98
+ });
99
+ describe('batchGetProcessStartTimes', ()=>{
100
+ beforeEach(()=>{
101
+ mockedExecFileSync.mockReset();
102
+ });
103
+ it('should parse ps lstart output', ()=>{
104
+ mockedExecFileSync.mockReturnValue(' 78070 Wed Mar 18 23:18:01 2026\n' + ' 55106 Mon Mar 9 21:41:42 2026\n');
105
+ const times = batchGetProcessStartTimes([
106
+ 78070,
107
+ 55106
108
+ ]);
109
+ expect(times.size).toBe(2);
110
+ expect(times.get(78070)?.getFullYear()).toBe(2026);
111
+ expect(times.get(55106)?.getMonth()).toBe(2); // March = 2
112
+ });
113
+ it('should return empty map for empty pids', ()=>{
114
+ expect(batchGetProcessStartTimes([])).toEqual(new Map());
115
+ });
116
+ it('should skip lines with unparseable dates', ()=>{
117
+ mockedExecFileSync.mockReturnValue(' 78070 Wed Mar 18 23:18:01 2026\n' + ' 99999 INVALID_DATE\n');
118
+ const times = batchGetProcessStartTimes([
119
+ 78070,
120
+ 99999
121
+ ]);
122
+ expect(times.size).toBe(1);
123
+ expect(times.has(78070)).toBe(true);
124
+ });
125
+ it('should return empty map on failure', ()=>{
126
+ mockedExecFileSync.mockImplementation(()=>{
127
+ throw new Error('fail');
128
+ });
129
+ expect(batchGetProcessStartTimes([
130
+ 78070
131
+ ])).toEqual(new Map());
132
+ });
133
+ });
134
+ describe('enrichProcesses', ()=>{
135
+ beforeEach(()=>{
136
+ mockedExecFileSync.mockReset();
137
+ });
138
+ it('should populate cwd and startTime on processes', ()=>{
139
+ // First call: batchGetProcessCwds (lsof)
140
+ // Second call: batchGetProcessStartTimes (ps lstart)
141
+ mockedExecFileSync.mockReturnValueOnce('p100\nn/projects/app\n').mockReturnValueOnce(' 100 Wed Mar 18 23:18:01 2026\n');
142
+ const processes = [
143
+ {
144
+ pid: 100,
145
+ command: 'claude',
146
+ cwd: '',
147
+ tty: 's001'
148
+ }
149
+ ];
150
+ const enriched = enrichProcesses(processes);
151
+ expect(enriched[0].cwd).toBe('/projects/app');
152
+ expect(enriched[0].startTime).toBeDefined();
153
+ });
154
+ it('should return empty array for empty input', ()=>{
155
+ expect(enrichProcesses([])).toEqual([]);
156
+ expect(mockedExecFileSync).not.toHaveBeenCalled();
157
+ });
158
+ it('should handle partial failures', ()=>{
159
+ // lsof succeeds, ps lstart fails
160
+ mockedExecFileSync.mockReturnValueOnce('p100\nn/projects/app\n').mockImplementationOnce(()=>{
161
+ throw new Error('fail');
162
+ });
163
+ const processes = [
164
+ {
165
+ pid: 100,
166
+ command: 'claude',
167
+ cwd: '',
168
+ tty: 's001'
169
+ }
170
+ ];
171
+ const enriched = enrichProcesses(processes);
172
+ expect(enriched[0].cwd).toBe('/projects/app');
173
+ expect(enriched[0].startTime).toBeUndefined();
174
+ });
175
+ });
176
+
177
+ //# sourceMappingURL=process.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/utils/process.test.ts"],"sourcesContent":["/**\n * Tests for new functions in utils/process.ts\n */\n\nimport type { MockedFunction } from 'vitest';\n\nimport { execFileSync } from 'child_process';\nimport {\n listAgentProcesses,\n batchGetProcessCwds,\n batchGetProcessStartTimes,\n enrichProcesses,\n} from '../../utils/process.js';\n\nvi.mock('child_process', () => ({\n execFileSync: vi.fn(),\n}));\n\nconst mockedExecFileSync = execFileSync as MockedFunction<typeof execFileSync>;\n\ndescribe('listAgentProcesses', () => {\n beforeEach(() => {\n mockedExecFileSync.mockReset();\n });\n\n it('should parse ps aux | grep output and post-filter by executable name', () => {\n mockedExecFileSync.mockReturnValue(\n 'user 78070 1.0 0.5 485636016 245952 s018 S+ 11:18PM 1:55.14 claude\\n' +\n 'user 55106 0.1 0.4 485620368 72496 s015 S+ 9Mar26 8:06.36 claude\\n',\n );\n\n const processes = listAgentProcesses('claude');\n expect(processes).toHaveLength(2);\n expect(processes[0].pid).toBe(78070);\n expect(processes[0].command).toBe('claude');\n expect(processes[0].tty).toBe('s018');\n expect(processes[0].cwd).toBe(''); // not populated yet\n expect(processes[1].pid).toBe(55106);\n });\n\n it('should filter out non-matching executables', () => {\n mockedExecFileSync.mockReturnValue(\n 'user 100 0.0 0.0 0 0 s001 S 1:00PM 0:00 claude\\n' +\n 'user 200 0.0 0.0 0 0 s002 S 1:00PM 0:00 claude-helper --pid 100\\n' +\n 'user 300 0.0 0.0 0 0 s003 S 1:00PM 0:00 /usr/bin/claude\\n',\n );\n\n const processes = listAgentProcesses('claude');\n expect(processes).toHaveLength(2);\n expect(processes.map(p => p.pid)).toEqual([100, 300]);\n });\n\n it('should return empty array on command failure', () => {\n mockedExecFileSync.mockImplementation(() => { throw new Error('fail'); });\n expect(listAgentProcesses('claude')).toEqual([]);\n });\n\n it('should handle empty output', () => {\n mockedExecFileSync.mockReturnValue('');\n expect(listAgentProcesses('claude')).toEqual([]);\n });\n\n it('should reject empty pattern', () => {\n expect(listAgentProcesses('')).toEqual([]);\n expect(mockedExecFileSync).not.toHaveBeenCalled();\n });\n\n it('should reject patterns with shell injection characters', () => {\n expect(listAgentProcesses('claude; rm -rf /')).toEqual([]);\n expect(listAgentProcesses(\"claude' || true\")).toEqual([]);\n expect(listAgentProcesses('$(whoami)')).toEqual([]);\n expect(mockedExecFileSync).not.toHaveBeenCalled();\n });\n\n it('should accept valid patterns with dashes and underscores', () => {\n mockedExecFileSync.mockReturnValue('');\n listAgentProcesses('claude-code');\n expect(mockedExecFileSync).toHaveBeenCalled();\n\n mockedExecFileSync.mockReset();\n mockedExecFileSync.mockReturnValue('');\n listAgentProcesses('my_agent');\n expect(mockedExecFileSync).toHaveBeenCalled();\n });\n});\n\ndescribe('batchGetProcessCwds', () => {\n beforeEach(() => {\n mockedExecFileSync.mockReset();\n });\n\n it('should parse batched lsof output', () => {\n mockedExecFileSync.mockReturnValue(\n 'p78070\\nn/Users/user/ai-devkit\\np55106\\nn/Users/user/other-project\\n',\n );\n\n const cwds = batchGetProcessCwds([78070, 55106]);\n expect(cwds.get(78070)).toBe('/Users/user/ai-devkit');\n expect(cwds.get(55106)).toBe('/Users/user/other-project');\n });\n\n it('should return empty map for empty pids', () => {\n expect(batchGetProcessCwds([])).toEqual(new Map());\n });\n\n it('should return partial results when lsof succeeds for some PIDs', () => {\n // lsof might not return entries for dead processes\n mockedExecFileSync.mockReturnValue(\n 'p78070\\nn/Users/user/ai-devkit\\n',\n );\n\n const cwds = batchGetProcessCwds([78070, 99999]);\n expect(cwds.size).toBe(1);\n expect(cwds.get(78070)).toBe('/Users/user/ai-devkit');\n });\n\n it('should return empty map on total failure', () => {\n mockedExecFileSync.mockImplementation(() => { throw new Error('fail'); });\n const cwds = batchGetProcessCwds([78070]);\n // Falls through to pwdx fallback which also fails\n expect(cwds.size).toBe(0);\n });\n});\n\ndescribe('batchGetProcessStartTimes', () => {\n beforeEach(() => {\n mockedExecFileSync.mockReset();\n });\n\n it('should parse ps lstart output', () => {\n mockedExecFileSync.mockReturnValue(\n ' 78070 Wed Mar 18 23:18:01 2026\\n' +\n ' 55106 Mon Mar 9 21:41:42 2026\\n',\n );\n\n const times = batchGetProcessStartTimes([78070, 55106]);\n expect(times.size).toBe(2);\n expect(times.get(78070)?.getFullYear()).toBe(2026);\n expect(times.get(55106)?.getMonth()).toBe(2); // March = 2\n });\n\n it('should return empty map for empty pids', () => {\n expect(batchGetProcessStartTimes([])).toEqual(new Map());\n });\n\n it('should skip lines with unparseable dates', () => {\n mockedExecFileSync.mockReturnValue(\n ' 78070 Wed Mar 18 23:18:01 2026\\n' +\n ' 99999 INVALID_DATE\\n',\n );\n\n const times = batchGetProcessStartTimes([78070, 99999]);\n expect(times.size).toBe(1);\n expect(times.has(78070)).toBe(true);\n });\n\n it('should return empty map on failure', () => {\n mockedExecFileSync.mockImplementation(() => { throw new Error('fail'); });\n expect(batchGetProcessStartTimes([78070])).toEqual(new Map());\n });\n});\n\ndescribe('enrichProcesses', () => {\n beforeEach(() => {\n mockedExecFileSync.mockReset();\n });\n\n it('should populate cwd and startTime on processes', () => {\n // First call: batchGetProcessCwds (lsof)\n // Second call: batchGetProcessStartTimes (ps lstart)\n mockedExecFileSync\n .mockReturnValueOnce('p100\\nn/projects/app\\n')\n .mockReturnValueOnce(' 100 Wed Mar 18 23:18:01 2026\\n');\n\n const processes = [\n { pid: 100, command: 'claude', cwd: '', tty: 's001' },\n ];\n\n const enriched = enrichProcesses(processes);\n expect(enriched[0].cwd).toBe('/projects/app');\n expect(enriched[0].startTime).toBeDefined();\n });\n\n it('should return empty array for empty input', () => {\n expect(enrichProcesses([])).toEqual([]);\n expect(mockedExecFileSync).not.toHaveBeenCalled();\n });\n\n it('should handle partial failures', () => {\n // lsof succeeds, ps lstart fails\n mockedExecFileSync\n .mockReturnValueOnce('p100\\nn/projects/app\\n')\n .mockImplementationOnce(() => { throw new Error('fail'); });\n\n const processes = [\n { pid: 100, command: 'claude', cwd: '', tty: 's001' },\n ];\n\n const enriched = enrichProcesses(processes);\n expect(enriched[0].cwd).toBe('/projects/app');\n expect(enriched[0].startTime).toBeUndefined();\n });\n});\n"],"names":["execFileSync","listAgentProcesses","batchGetProcessCwds","batchGetProcessStartTimes","enrichProcesses","vi","mock","fn","mockedExecFileSync","describe","beforeEach","mockReset","it","mockReturnValue","processes","expect","toHaveLength","pid","toBe","command","tty","cwd","map","p","toEqual","mockImplementation","Error","not","toHaveBeenCalled","cwds","get","Map","size","times","getFullYear","getMonth","has","mockReturnValueOnce","enriched","startTime","toBeDefined","mockImplementationOnce","toBeUndefined"],"mappings":"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"}