@ai-devkit/agent-manager 0.14.0 → 0.16.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.
- package/.eslintrc.json +1 -1
- package/dist/AgentManager.d.ts +14 -12
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +40 -33
- package/dist/AgentManager.js.map +1 -1
- package/dist/adapters/AgentAdapter.js +2 -5
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +29 -63
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +5 -2
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +66 -73
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +5 -2
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +63 -70
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.d.ts +1 -1
- package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +20 -59
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +6 -6
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +5 -13
- package/dist/adapters/index.js.map +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -22
- package/dist/index.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +13 -17
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TmuxManager.d.ts +28 -0
- package/dist/terminal/TmuxManager.d.ts.map +1 -0
- package/dist/terminal/TmuxManager.js +112 -0
- package/dist/terminal/TmuxManager.js.map +1 -0
- package/dist/terminal/TtyWriter.d.ts +1 -1
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +11 -15
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/terminal/index.d.ts +4 -4
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +3 -9
- package/dist/terminal/index.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts +35 -0
- package/dist/utils/AgentRegistry.d.ts.map +1 -0
- package/dist/utils/AgentRegistry.js +114 -0
- package/dist/utils/AgentRegistry.js.map +1 -0
- package/dist/utils/ClaudeSessionParser.d.ts +2 -2
- package/dist/utils/ClaudeSessionParser.d.ts.map +1 -1
- package/dist/utils/ClaudeSessionParser.js +32 -59
- package/dist/utils/ClaudeSessionParser.js.map +1 -1
- package/dist/utils/agents.d.ts +15 -0
- package/dist/utils/agents.d.ts.map +1 -0
- package/dist/utils/agents.js +30 -0
- package/dist/utils/agents.js.map +1 -0
- package/dist/utils/applescript.js +1 -4
- package/dist/utils/applescript.js.map +1 -1
- package/dist/utils/index.d.ts +6 -6
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -15
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.d.ts +2 -2
- package/dist/utils/matching.d.ts.map +1 -1
- package/dist/utils/matching.js +3 -40
- package/dist/utils/matching.js.map +1 -1
- package/dist/utils/process.d.ts +1 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +12 -52
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.js +8 -49
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/sortAgents.d.ts +4 -0
- package/dist/utils/sortAgents.d.ts.map +1 -0
- package/dist/utils/sortAgents.js +48 -0
- package/dist/utils/sortAgents.js.map +1 -0
- package/package.json +9 -8
- package/src/AgentManager.ts +53 -28
- package/src/__tests__/AgentManager.test.ts +140 -5
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +45 -44
- package/src/__tests__/adapters/CodexAdapter.test.ts +137 -30
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +130 -18
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +23 -21
- package/src/__tests__/terminal/TmuxManager.test.ts +175 -0
- package/src/__tests__/terminal/TtyWriter.test.ts +10 -9
- package/src/__tests__/utils/AgentRegistry.test.ts +223 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +73 -2
- package/src/__tests__/utils/matching.test.ts +4 -4
- package/src/__tests__/utils/process.test.ts +6 -5
- package/src/__tests__/utils/session.test.ts +8 -7
- package/src/adapters/ClaudeCodeAdapter.ts +8 -8
- package/src/adapters/CodexAdapter.ts +51 -12
- package/src/adapters/GeminiCliAdapter.ts +51 -12
- package/src/adapters/OpenCodeAdapter.ts +4 -4
- package/src/adapters/index.ts +6 -6
- package/src/index.ts +19 -11
- package/src/terminal/TerminalFocusManager.ts +2 -2
- package/src/terminal/TmuxManager.ts +118 -0
- package/src/terminal/TtyWriter.ts +3 -3
- package/src/terminal/index.ts +4 -4
- package/src/utils/AgentRegistry.ts +139 -0
- package/src/utils/ClaudeSessionParser.ts +23 -12
- package/src/utils/agents.ts +41 -0
- package/src/utils/index.ts +6 -6
- package/src/utils/matching.ts +2 -2
- package/src/utils/process.ts +1 -1
- package/src/utils/sortAgents.ts +52 -0
- package/tsconfig.json +2 -2
- package/vitest.config.ts +20 -0
- package/jest.config.js +0 -21
|
@@ -2,16 +2,20 @@
|
|
|
2
2
|
* Tests for AgentManager
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
import
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { AgentManager } from '../AgentManager.js';
|
|
7
10
|
import type {
|
|
8
11
|
AgentAdapter,
|
|
9
12
|
AgentInfo,
|
|
10
13
|
AgentType,
|
|
11
14
|
ConversationMessage,
|
|
12
15
|
SessionSummary,
|
|
13
|
-
} from '../adapters/AgentAdapter';
|
|
14
|
-
import { AgentStatus } from '../adapters/AgentAdapter';
|
|
16
|
+
} from '../adapters/AgentAdapter.js';
|
|
17
|
+
import { AgentStatus } from '../adapters/AgentAdapter.js';
|
|
18
|
+
import { AgentRegistry, type RegistryEntry } from '../utils/AgentRegistry.js';
|
|
15
19
|
|
|
16
20
|
// Mock adapter for testing
|
|
17
21
|
class MockAdapter implements AgentAdapter {
|
|
@@ -240,6 +244,137 @@ describe('AgentManager', () => {
|
|
|
240
244
|
});
|
|
241
245
|
});
|
|
242
246
|
|
|
247
|
+
describe('listAgents — registry persistence', () => {
|
|
248
|
+
let tmpDir: string;
|
|
249
|
+
let regPath: string;
|
|
250
|
+
let registry: AgentRegistry;
|
|
251
|
+
let scopedManager: AgentManager;
|
|
252
|
+
|
|
253
|
+
beforeEach(() => {
|
|
254
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-manager-'));
|
|
255
|
+
regPath = path.join(tmpDir, 'agents.json');
|
|
256
|
+
registry = new AgentRegistry(regPath);
|
|
257
|
+
scopedManager = new AgentManager(registry);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
afterEach(() => {
|
|
261
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('persists every detected agent to the registry', async () => {
|
|
265
|
+
scopedManager.registerAdapter(new MockAdapter('claude', [
|
|
266
|
+
createMockAgent({
|
|
267
|
+
name: 'a',
|
|
268
|
+
pid: process.pid,
|
|
269
|
+
sessionId: 'sid-a',
|
|
270
|
+
sessionFilePath: '/path/a.jsonl',
|
|
271
|
+
projectPath: '/cwd/a',
|
|
272
|
+
}),
|
|
273
|
+
]));
|
|
274
|
+
|
|
275
|
+
await scopedManager.listAgents();
|
|
276
|
+
|
|
277
|
+
const entries = registry.list();
|
|
278
|
+
expect(entries).toHaveLength(1);
|
|
279
|
+
expect(entries[0]).toMatchObject({
|
|
280
|
+
name: 'a',
|
|
281
|
+
type: 'claude',
|
|
282
|
+
pid: process.pid,
|
|
283
|
+
cwd: '/cwd/a',
|
|
284
|
+
sessionId: 'sid-a',
|
|
285
|
+
sessionFilePath: '/path/a.jsonl',
|
|
286
|
+
tmuxSession: '',
|
|
287
|
+
});
|
|
288
|
+
expect(entries[0].startedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('prunes entries for dead pids', async () => {
|
|
292
|
+
registry.register({
|
|
293
|
+
name: 'dead',
|
|
294
|
+
type: 'claude',
|
|
295
|
+
pid: 999999,
|
|
296
|
+
tmuxSession: '',
|
|
297
|
+
cwd: '/cwd/dead',
|
|
298
|
+
startedAt: '2026-05-30T00:00:00.000Z',
|
|
299
|
+
sessionId: 'sid-dead',
|
|
300
|
+
sessionFilePath: '/path/dead.jsonl',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
scopedManager.registerAdapter(new MockAdapter('claude', [
|
|
304
|
+
createMockAgent({ name: 'live', pid: process.pid }),
|
|
305
|
+
]));
|
|
306
|
+
|
|
307
|
+
await scopedManager.listAgents();
|
|
308
|
+
|
|
309
|
+
const entries = registry.list();
|
|
310
|
+
expect(entries.map((e) => e.name)).toEqual(['live']);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('preserves an existing name (e.g. user-set "merry") across cycles', async () => {
|
|
314
|
+
registry.register({
|
|
315
|
+
name: 'merry',
|
|
316
|
+
type: 'claude',
|
|
317
|
+
pid: process.pid,
|
|
318
|
+
tmuxSession: 'merry',
|
|
319
|
+
cwd: '/cwd/merry',
|
|
320
|
+
startedAt: '2026-05-30T00:00:00.000Z',
|
|
321
|
+
sessionId: 'sid-merry',
|
|
322
|
+
sessionFilePath: '/path/merry.jsonl',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
scopedManager.registerAdapter(new MockAdapter('claude', [
|
|
326
|
+
createMockAgent({ name: 'default-name', pid: process.pid }),
|
|
327
|
+
]));
|
|
328
|
+
|
|
329
|
+
const agents = await scopedManager.listAgents();
|
|
330
|
+
|
|
331
|
+
expect(agents[0].name).toBe('merry');
|
|
332
|
+
expect(registry.list()[0].name).toBe('merry');
|
|
333
|
+
expect(registry.list()[0].tmuxSession).toBe('merry');
|
|
334
|
+
expect(registry.list()[0].startedAt).toBe('2026-05-30T00:00:00.000Z');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('writes a fresh startedAt for new entries', async () => {
|
|
338
|
+
const before = new Date().toISOString();
|
|
339
|
+
scopedManager.registerAdapter(new MockAdapter('claude', [
|
|
340
|
+
createMockAgent({ name: 'new', pid: process.pid }),
|
|
341
|
+
]));
|
|
342
|
+
|
|
343
|
+
await scopedManager.listAgents();
|
|
344
|
+
|
|
345
|
+
const entry = registry.list()[0];
|
|
346
|
+
expect(entry.startedAt >= before).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('batches the write — a single registerBatch call per listAgents', async () => {
|
|
350
|
+
const spy = vi.spyOn(registry, 'registerBatch');
|
|
351
|
+
|
|
352
|
+
scopedManager.registerAdapter(new MockAdapter('claude', [
|
|
353
|
+
createMockAgent({ name: 'a', pid: process.pid }),
|
|
354
|
+
]));
|
|
355
|
+
scopedManager.registerAdapter(new MockAdapter('codex', [
|
|
356
|
+
createMockAgent({ name: 'b', type: 'codex', pid: process.pid + 1 }),
|
|
357
|
+
]));
|
|
358
|
+
|
|
359
|
+
await scopedManager.listAgents();
|
|
360
|
+
|
|
361
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
362
|
+
expect((spy.mock.calls[0][0] as RegistryEntry[]).map((e) => e.name).sort())
|
|
363
|
+
.toEqual(['a', 'b']);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('skips registerBatch when no agents detected (still calls prune)', async () => {
|
|
367
|
+
const writeSpy = vi.spyOn(registry, 'registerBatch');
|
|
368
|
+
const pruneSpy = vi.spyOn(registry, 'prune');
|
|
369
|
+
|
|
370
|
+
scopedManager.registerAdapter(new MockAdapter('claude', []));
|
|
371
|
+
await scopedManager.listAgents();
|
|
372
|
+
|
|
373
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
374
|
+
expect(pruneSpy).toHaveBeenCalledTimes(1);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
243
378
|
describe('clear', () => {
|
|
244
379
|
it('should remove all adapters', () => {
|
|
245
380
|
manager.registerAdapter(new MockAdapter('claude'));
|
|
@@ -333,7 +468,7 @@ describe('AgentManager', () => {
|
|
|
333
468
|
new MockAdapter('codex', [], false, [], true /* failListSessions */),
|
|
334
469
|
);
|
|
335
470
|
|
|
336
|
-
const consoleErrorSpy =
|
|
471
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
337
472
|
|
|
338
473
|
try {
|
|
339
474
|
const result = await manager.listSessions();
|
|
@@ -2,40 +2,42 @@
|
|
|
2
2
|
* Tests for ClaudeCodeAdapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { MockedFunction } from 'vitest';
|
|
5
6
|
import * as fs from 'fs';
|
|
6
7
|
import * as path from 'path';
|
|
7
|
-
|
|
8
|
-
import { ClaudeCodeAdapter } from '../../adapters/ClaudeCodeAdapter';
|
|
9
|
-
import type { ProcessInfo } from '../../adapters/AgentAdapter';
|
|
10
|
-
import { AgentStatus } from '../../adapters/AgentAdapter';
|
|
11
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process';
|
|
12
|
-
import { batchGetSessionFileBirthtimes } from '../../utils/session';
|
|
13
|
-
import type { SessionFile } from '../../utils/session';
|
|
14
|
-
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching';
|
|
15
|
-
import type { MatchResult } from '../../utils/matching';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
|
|
9
|
+
import { ClaudeCodeAdapter } from '../../adapters/ClaudeCodeAdapter.js';
|
|
10
|
+
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
11
|
+
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
12
|
+
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
13
|
+
import { batchGetSessionFileBirthtimes } from '../../utils/session.js';
|
|
14
|
+
import type { SessionFile } from '../../utils/session.js';
|
|
15
|
+
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
16
|
+
import type { MatchResult } from '../../utils/matching.js';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
vi.mock('../../utils/process.js', () => ({
|
|
19
|
+
listAgentProcesses: vi.fn(),
|
|
20
|
+
enrichProcesses: vi.fn(),
|
|
19
21
|
}));
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
const actual =
|
|
23
|
+
vi.mock('../../utils/session.js', async () => {
|
|
24
|
+
const actual = await vi.importActual('../../utils/session') as typeof import('../../utils/session');
|
|
23
25
|
return {
|
|
24
26
|
...actual,
|
|
25
|
-
batchGetSessionFileBirthtimes:
|
|
27
|
+
batchGetSessionFileBirthtimes: vi.fn(),
|
|
26
28
|
};
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
matchProcessesToSessions:
|
|
31
|
-
generateAgentName:
|
|
31
|
+
vi.mock('../../utils/matching.js', () => ({
|
|
32
|
+
matchProcessesToSessions: vi.fn(),
|
|
33
|
+
generateAgentName: vi.fn(),
|
|
32
34
|
}));
|
|
33
35
|
|
|
34
|
-
const mockedListAgentProcesses = listAgentProcesses as
|
|
35
|
-
const mockedEnrichProcesses = enrichProcesses as
|
|
36
|
-
const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as
|
|
37
|
-
const mockedMatchProcessesToSessions = matchProcessesToSessions as
|
|
38
|
-
const mockedGenerateAgentName = generateAgentName as
|
|
36
|
+
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
37
|
+
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
38
|
+
const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as MockedFunction<typeof batchGetSessionFileBirthtimes>;
|
|
39
|
+
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
40
|
+
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
39
41
|
describe('ClaudeCodeAdapter', () => {
|
|
40
42
|
let adapter: ClaudeCodeAdapter;
|
|
41
43
|
|
|
@@ -163,7 +165,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
163
165
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
164
166
|
|
|
165
167
|
// Set up projects dir with encoded directory name
|
|
166
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
168
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-test-'));
|
|
167
169
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
168
170
|
// Claude encodes /Users/test/my-project → -Users-test-my-project
|
|
169
171
|
const projDir = path.join(projectsDir, '-Users-test-my-project');
|
|
@@ -222,7 +224,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
222
224
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
223
225
|
|
|
224
226
|
// Set up projects dir with encoded directory names
|
|
225
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
227
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-test-'));
|
|
226
228
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
227
229
|
// /project-a → -project-a, /project-b → -project-b
|
|
228
230
|
const projDirA = path.join(projectsDir, '-project-a');
|
|
@@ -303,7 +305,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
303
305
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
304
306
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
305
307
|
|
|
306
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
308
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-resume-'));
|
|
307
309
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
308
310
|
const projDir = path.join(projectsDir, '-project-resumed');
|
|
309
311
|
fs.mkdirSync(projDir, { recursive: true });
|
|
@@ -367,7 +369,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
367
369
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
368
370
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
369
371
|
|
|
370
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
372
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-pid-test-'));
|
|
371
373
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
372
374
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
373
375
|
const projDir = path.join(projectsDir, '-project-direct');
|
|
@@ -416,7 +418,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
416
418
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
417
419
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
418
420
|
|
|
419
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
421
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-pid-wait-'));
|
|
420
422
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
421
423
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
422
424
|
const projDir = path.join(projectsDir, '-project-wait');
|
|
@@ -425,8 +427,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
425
427
|
|
|
426
428
|
const sessionId = 'wait-session';
|
|
427
429
|
const jsonlPath = path.join(projDir, `${sessionId}.jsonl`);
|
|
428
|
-
//
|
|
429
|
-
// PID file's live status must win.
|
|
430
|
+
// PID file's live status must win over JSONL-derived status.
|
|
430
431
|
fs.writeFileSync(jsonlPath, [
|
|
431
432
|
JSON.stringify({ type: 'user', timestamp: new Date().toISOString(), cwd: '/project/wait', message: { content: '/reddit-commenter' } }),
|
|
432
433
|
JSON.stringify({ type: 'permission-mode', timestamp: new Date().toISOString(), permissionMode: 'default' }),
|
|
@@ -463,7 +464,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
463
464
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
464
465
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
465
466
|
|
|
466
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
467
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-pid-idle-'));
|
|
467
468
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
468
469
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
469
470
|
const projDir = path.join(projectsDir, '-project-idle');
|
|
@@ -508,7 +509,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
508
509
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
509
510
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
510
511
|
|
|
511
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
512
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-pid-nostat-'));
|
|
512
513
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
513
514
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
514
515
|
const projDir = path.join(projectsDir, '-project-legacy');
|
|
@@ -560,7 +561,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
560
561
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
561
562
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
562
563
|
|
|
563
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
564
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-resume-wait-'));
|
|
564
565
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
565
566
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
566
567
|
const projDir = path.join(projectsDir, '-project-resume-wait');
|
|
@@ -604,7 +605,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
604
605
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
605
606
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
606
607
|
|
|
607
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
608
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-gone-'));
|
|
608
609
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
609
610
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
610
611
|
const projDir = path.join(projectsDir, '-project-gone');
|
|
@@ -623,7 +624,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
623
624
|
(adapter as any).projectsDir = projectsDir;
|
|
624
625
|
|
|
625
626
|
// Simulate JSONL disappearing between existence check and read
|
|
626
|
-
|
|
627
|
+
vi.spyOn((adapter as any).parser, 'readSession').mockReturnValueOnce(null);
|
|
627
628
|
|
|
628
629
|
const agents = await adapter.detectAgents();
|
|
629
630
|
|
|
@@ -633,7 +634,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
633
634
|
expect(agents[0].status).toBe(AgentStatus.IDLE);
|
|
634
635
|
|
|
635
636
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
636
|
-
|
|
637
|
+
vi.restoreAllMocks();
|
|
637
638
|
});
|
|
638
639
|
|
|
639
640
|
it('should fall back to process-only when legacy-matched JSONL becomes unreadable', async () => {
|
|
@@ -644,7 +645,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
644
645
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
645
646
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
646
647
|
|
|
647
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
648
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-lgone-'));
|
|
648
649
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
649
650
|
const projDir = path.join(projectsDir, '-project-legacy-gone');
|
|
650
651
|
fs.mkdirSync(projDir, { recursive: true });
|
|
@@ -670,7 +671,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
670
671
|
]);
|
|
671
672
|
|
|
672
673
|
// Simulate JSONL disappearing between match and read
|
|
673
|
-
|
|
674
|
+
vi.spyOn((adapter as any).parser, 'readSession').mockReturnValueOnce(null);
|
|
674
675
|
|
|
675
676
|
const agents = await adapter.detectAgents();
|
|
676
677
|
|
|
@@ -679,7 +680,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
679
680
|
expect(agents[0].status).toBe(AgentStatus.IDLE);
|
|
680
681
|
|
|
681
682
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
682
|
-
|
|
683
|
+
vi.restoreAllMocks();
|
|
683
684
|
});
|
|
684
685
|
|
|
685
686
|
it('should mix direct PID-file matches and legacy matches across processes', async () => {
|
|
@@ -691,7 +692,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
691
692
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
692
693
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
693
694
|
|
|
694
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
695
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-mix-test-'));
|
|
695
696
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
696
697
|
const projectsDir = path.join(tmpDir, 'projects');
|
|
697
698
|
const projAlpha = path.join(projectsDir, '-project-alpha');
|
|
@@ -756,7 +757,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
756
757
|
let tmpDir: string;
|
|
757
758
|
|
|
758
759
|
beforeEach(() => {
|
|
759
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
760
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-test-'));
|
|
760
761
|
});
|
|
761
762
|
|
|
762
763
|
afterEach(() => {
|
|
@@ -1071,7 +1072,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
1071
1072
|
let tmpDir: string;
|
|
1072
1073
|
|
|
1073
1074
|
beforeEach(() => {
|
|
1074
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
1075
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-test-'));
|
|
1075
1076
|
});
|
|
1076
1077
|
|
|
1077
1078
|
afterEach(() => {
|
|
@@ -1419,7 +1420,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
1419
1420
|
let tmpDir: string;
|
|
1420
1421
|
|
|
1421
1422
|
beforeEach(() => {
|
|
1422
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
1423
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-conv-'));
|
|
1423
1424
|
});
|
|
1424
1425
|
|
|
1425
1426
|
afterEach(() => {
|
|
@@ -1594,7 +1595,7 @@ describe('ClaudeCodeAdapter', () => {
|
|
|
1594
1595
|
let projectsDir: string;
|
|
1595
1596
|
|
|
1596
1597
|
beforeEach(() => {
|
|
1597
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
1598
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-list-'));
|
|
1598
1599
|
projectsDir = path.join(tmpDir, 'projects');
|
|
1599
1600
|
fs.mkdirSync(projectsDir, { recursive: true });
|
|
1600
1601
|
(adapter as any).projectsDir = projectsDir;
|
|
@@ -2,41 +2,44 @@
|
|
|
2
2
|
* Tests for CodexAdapter
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { MockedFunction } from 'vitest';
|
|
5
6
|
import * as fs from 'fs';
|
|
6
7
|
import * as path from 'path';
|
|
7
|
-
|
|
8
|
-
import { CodexAdapter } from '../../adapters/CodexAdapter';
|
|
9
|
-
import type { ProcessInfo } from '../../adapters/AgentAdapter';
|
|
10
|
-
import { AgentStatus } from '../../adapters/AgentAdapter';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
8
|
+
|
|
9
|
+
import { CodexAdapter } from '../../adapters/CodexAdapter.js';
|
|
10
|
+
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
11
|
+
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
12
|
+
import { AgentRegistry, type RegistryEntry } from '../../utils/AgentRegistry.js';
|
|
13
|
+
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
14
|
+
import { batchGetSessionFileBirthtimes } from '../../utils/session.js';
|
|
15
|
+
import type { SessionFile } from '../../utils/session.js';
|
|
16
|
+
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
17
|
+
import type { MatchResult } from '../../utils/matching.js';
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
|
|
20
|
+
vi.mock('../../utils/process.js', () => ({
|
|
21
|
+
listAgentProcesses: vi.fn(),
|
|
22
|
+
enrichProcesses: vi.fn(),
|
|
20
23
|
}));
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
const actual =
|
|
25
|
+
vi.mock('../../utils/session.js', async () => {
|
|
26
|
+
const actual = await vi.importActual('../../utils/session') as typeof import('../../utils/session');
|
|
24
27
|
return {
|
|
25
28
|
...actual,
|
|
26
|
-
batchGetSessionFileBirthtimes:
|
|
29
|
+
batchGetSessionFileBirthtimes: vi.fn(),
|
|
27
30
|
};
|
|
28
31
|
});
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
matchProcessesToSessions:
|
|
32
|
-
generateAgentName:
|
|
33
|
+
vi.mock('../../utils/matching.js', () => ({
|
|
34
|
+
matchProcessesToSessions: vi.fn(),
|
|
35
|
+
generateAgentName: vi.fn(),
|
|
33
36
|
}));
|
|
34
37
|
|
|
35
|
-
const mockedListAgentProcesses = listAgentProcesses as
|
|
36
|
-
const mockedEnrichProcesses = enrichProcesses as
|
|
37
|
-
const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as
|
|
38
|
-
const mockedMatchProcessesToSessions = matchProcessesToSessions as
|
|
39
|
-
const mockedGenerateAgentName = generateAgentName as
|
|
38
|
+
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
39
|
+
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
40
|
+
const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as MockedFunction<typeof batchGetSessionFileBirthtimes>;
|
|
41
|
+
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
42
|
+
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
40
43
|
|
|
41
44
|
describe('CodexAdapter', () => {
|
|
42
45
|
let adapter: CodexAdapter;
|
|
@@ -136,7 +139,7 @@ describe('CodexAdapter', () => {
|
|
|
136
139
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
137
140
|
|
|
138
141
|
// Set up sessions dir with date directory
|
|
139
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
142
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
|
|
140
143
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
141
144
|
const dateDir = path.join(sessionsDir, '2026', '03', '18');
|
|
142
145
|
fs.mkdirSync(dateDir, { recursive: true });
|
|
@@ -195,7 +198,7 @@ describe('CodexAdapter', () => {
|
|
|
195
198
|
mockedListAgentProcesses.mockReturnValue(processes);
|
|
196
199
|
mockedEnrichProcesses.mockReturnValue(processes);
|
|
197
200
|
|
|
198
|
-
const tmpDir = fs.mkdtempSync(path.join(
|
|
201
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
|
|
199
202
|
const sessionsDir = path.join(tmpDir, 'sessions');
|
|
200
203
|
const now = new Date();
|
|
201
204
|
const dateDir = path.join(
|
|
@@ -247,11 +250,115 @@ describe('CodexAdapter', () => {
|
|
|
247
250
|
});
|
|
248
251
|
});
|
|
249
252
|
|
|
253
|
+
describe('detectAgents — registry cache short-circuit', () => {
|
|
254
|
+
let tmpDir: string;
|
|
255
|
+
let regPath: string;
|
|
256
|
+
let registry: AgentRegistry;
|
|
257
|
+
let cachedAdapter: CodexAdapter;
|
|
258
|
+
let sessionFilePath: string;
|
|
259
|
+
|
|
260
|
+
function registerEntry(over: Partial<RegistryEntry> = {}): void {
|
|
261
|
+
registry.register({
|
|
262
|
+
name: 'codex-100',
|
|
263
|
+
type: 'codex',
|
|
264
|
+
pid: 100,
|
|
265
|
+
tmuxSession: '',
|
|
266
|
+
cwd: '/repo-a',
|
|
267
|
+
startedAt: '2026-05-30T00:00:00.000Z',
|
|
268
|
+
sessionId: 'sess-cached',
|
|
269
|
+
sessionFilePath,
|
|
270
|
+
...over,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
beforeEach(() => {
|
|
275
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-cache-'));
|
|
276
|
+
regPath = path.join(tmpDir, 'agents.json');
|
|
277
|
+
registry = new AgentRegistry(regPath);
|
|
278
|
+
cachedAdapter = new CodexAdapter(registry);
|
|
279
|
+
|
|
280
|
+
const recentTs = new Date().toISOString();
|
|
281
|
+
sessionFilePath = path.join(tmpDir, 'sess-cached.jsonl');
|
|
282
|
+
fs.writeFileSync(sessionFilePath, [
|
|
283
|
+
JSON.stringify({ type: 'session_meta', payload: { id: 'sess-cached', timestamp: recentTs, cwd: '/repo-a' } }),
|
|
284
|
+
JSON.stringify({ type: 'event', timestamp: recentTs, payload: { type: 'token_count', message: 'Hello from cache' } }),
|
|
285
|
+
].join('\n'));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
afterEach(() => {
|
|
289
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('short-circuits matching when registry has a valid entry', async () => {
|
|
293
|
+
registerEntry();
|
|
294
|
+
const processes: ProcessInfo[] = [
|
|
295
|
+
{ pid: 100, command: 'codex', cwd: '/repo-a', tty: 'ttys001', startTime: new Date() },
|
|
296
|
+
];
|
|
297
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
298
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
299
|
+
|
|
300
|
+
const agents = await cachedAdapter.detectAgents();
|
|
301
|
+
|
|
302
|
+
expect(agents).toHaveLength(1);
|
|
303
|
+
expect(agents[0]).toMatchObject({
|
|
304
|
+
type: 'codex',
|
|
305
|
+
pid: 100,
|
|
306
|
+
sessionId: 'sess-cached',
|
|
307
|
+
sessionFilePath,
|
|
308
|
+
summary: 'Hello from cache',
|
|
309
|
+
});
|
|
310
|
+
expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
|
|
311
|
+
expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('falls through when no registry entry exists for the pid', async () => {
|
|
315
|
+
const processes: ProcessInfo[] = [
|
|
316
|
+
{ pid: 100, command: 'codex', cwd: '/repo-a', tty: 'ttys001', startTime: new Date() },
|
|
317
|
+
];
|
|
318
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
319
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
320
|
+
(cachedAdapter as any).codexSessionsDir = '/nonexistent';
|
|
321
|
+
|
|
322
|
+
const agents = await cachedAdapter.detectAgents();
|
|
323
|
+
|
|
324
|
+
expect(agents[0].sessionId).toBe('pid-100');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('falls through when registry entry type does not match', async () => {
|
|
328
|
+
registerEntry({ type: 'claude' });
|
|
329
|
+
const processes: ProcessInfo[] = [
|
|
330
|
+
{ pid: 100, command: 'codex', cwd: '/repo-a', tty: 'ttys001', startTime: new Date() },
|
|
331
|
+
];
|
|
332
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
333
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
334
|
+
(cachedAdapter as any).codexSessionsDir = '/nonexistent';
|
|
335
|
+
|
|
336
|
+
const agents = await cachedAdapter.detectAgents();
|
|
337
|
+
|
|
338
|
+
expect(agents[0].sessionId).toBe('pid-100');
|
|
339
|
+
expect(mockedMatchProcessesToSessions).not.toHaveBeenCalled();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('falls through when the cached session file no longer exists', async () => {
|
|
343
|
+
registerEntry({ sessionFilePath: path.join(tmpDir, 'deleted.jsonl') });
|
|
344
|
+
const processes: ProcessInfo[] = [
|
|
345
|
+
{ pid: 100, command: 'codex', cwd: '/repo-a', tty: 'ttys001', startTime: new Date() },
|
|
346
|
+
];
|
|
347
|
+
mockedListAgentProcesses.mockReturnValue(processes);
|
|
348
|
+
mockedEnrichProcesses.mockReturnValue(processes);
|
|
349
|
+
(cachedAdapter as any).codexSessionsDir = '/nonexistent';
|
|
350
|
+
|
|
351
|
+
const agents = await cachedAdapter.detectAgents();
|
|
352
|
+
|
|
353
|
+
expect(agents[0].sessionId).toBe('pid-100');
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
250
357
|
describe('discoverSessions', () => {
|
|
251
358
|
let tmpDir: string;
|
|
252
359
|
|
|
253
360
|
beforeEach(() => {
|
|
254
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
361
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
|
|
255
362
|
});
|
|
256
363
|
|
|
257
364
|
afterEach(() => {
|
|
@@ -399,7 +506,7 @@ describe('CodexAdapter', () => {
|
|
|
399
506
|
let tmpDir: string;
|
|
400
507
|
|
|
401
508
|
beforeEach(() => {
|
|
402
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
509
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
|
|
403
510
|
});
|
|
404
511
|
|
|
405
512
|
afterEach(() => {
|
|
@@ -524,7 +631,7 @@ describe('CodexAdapter', () => {
|
|
|
524
631
|
let tmpDir: string;
|
|
525
632
|
|
|
526
633
|
beforeEach(() => {
|
|
527
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
634
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-conv-'));
|
|
528
635
|
});
|
|
529
636
|
|
|
530
637
|
afterEach(() => {
|
|
@@ -640,7 +747,7 @@ describe('CodexAdapter', () => {
|
|
|
640
747
|
let sessionsDir: string;
|
|
641
748
|
|
|
642
749
|
beforeEach(() => {
|
|
643
|
-
tmpDir = fs.mkdtempSync(path.join(
|
|
750
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-list-'));
|
|
644
751
|
sessionsDir = path.join(tmpDir, 'sessions');
|
|
645
752
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
646
753
|
(adapter as any).codexSessionsDir = sessionsDir;
|