@ai-devkit/agent-manager 0.29.0 → 0.30.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/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +1 -342
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/database/DurableAgentsDatabase.test.js +31 -3
- package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -1
- package/dist/__tests__/durable/CodexCliProbe.test.js +129 -0
- package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +84 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +126 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js +271 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js +393 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -0
- package/dist/__tests__/print/ClaudeCliProbe.test.js +3 -7
- package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +25 -28
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgentService.test.js +65 -0
- package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintRunner.test.js +4 -10
- package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +79 -0
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +436 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +76 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +43 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +202 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +119 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +386 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +34 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js +2 -2
- package/dist/__tests__/utils/ClaudeSessionParser.test.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 +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/dist/durable/DurableAgent.d.ts +23 -3
- package/dist/durable/DurableAgent.d.ts.map +1 -1
- package/dist/durable/DurableAgent.js +12 -0
- package/dist/durable/DurableAgent.js.map +1 -1
- package/dist/durable/DurableAgentRepository.d.ts +6 -7
- package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
- package/dist/durable/DurableAgentRepository.js +73 -42
- package/dist/durable/DurableAgentRepository.js.map +1 -1
- package/dist/durable/process.d.ts +8 -0
- package/dist/durable/process.d.ts.map +1 -0
- package/dist/durable/process.js +40 -0
- package/dist/durable/process.js.map +1 -0
- package/dist/durable/run.d.ts +32 -0
- package/dist/durable/run.d.ts.map +1 -0
- package/dist/durable/run.js +46 -0
- package/dist/durable/run.js.map +1 -0
- package/dist/durable/utils.d.ts +19 -0
- package/dist/durable/utils.d.ts.map +1 -0
- package/dist/durable/utils.js +52 -0
- package/dist/durable/utils.js.map +1 -0
- package/dist/index.d.ts +17 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/ClaudeAgentMapper.d.ts +21 -0
- package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeAgentMapper.js +42 -0
- package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts +29 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js +129 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.d.ts → providers/claude/ClaudeSessionLocator.d.ts} +68 -52
- package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.js → providers/claude/ClaudeSessionLocator.js} +71 -189
- package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.d.ts +2 -2
- package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.js +1 -1
- package/dist/providers/claude/ClaudeSessionParser.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.js +4 -9
- package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintAgentService.d.ts +4 -9
- package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js +45 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintRunner.d.ts +3 -3
- package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js +75 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +17 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js +69 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts +20 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.js +61 -0
- package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +40 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js +50 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts +34 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js +78 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts +21 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.js +86 -0
- package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts +20 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.js +51 -0
- package/dist/providers/pi/durable/PiCliProbe.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts +39 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js +47 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts +4 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js +44 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts +32 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.js +61 -0
- package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts +21 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.js +71 -0
- package/dist/providers/pi/durable/PiStreamParser.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1 -307
- package/src/__tests__/database/DurableAgentsDatabase.test.ts +28 -3
- package/src/__tests__/durable/CodexCliProbe.test.ts +63 -0
- package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +54 -0
- package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +79 -0
- package/src/__tests__/durable/CodexPrintAgentService.test.ts +139 -0
- package/src/__tests__/durable/CodexPrintRunner.test.ts +189 -0
- package/src/__tests__/fixtures/fake-codex.cjs +45 -0
- package/src/__tests__/print/ClaudeCliProbe.test.ts +3 -7
- package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +11 -21
- package/src/__tests__/print/ClaudePrintAgentService.test.ts +33 -1
- package/src/__tests__/print/ClaudePrintRunner.test.ts +4 -10
- package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +84 -0
- package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +411 -0
- package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +38 -0
- package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +32 -0
- package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +69 -0
- package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +33 -0
- package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +107 -0
- package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +24 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +2 -2
- package/src/adapters/index.ts +1 -1
- package/src/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/src/durable/DurableAgent.ts +51 -3
- package/src/durable/DurableAgentRepository.ts +60 -31
- package/src/durable/process.ts +28 -0
- package/src/durable/run.ts +74 -0
- package/src/durable/utils.ts +61 -0
- package/src/index.ts +31 -15
- package/src/providers/claude/ClaudeAgentMapper.ts +62 -0
- package/src/providers/claude/ClaudeCodeAdapter.ts +160 -0
- package/src/{adapters/ClaudeCodeAdapter.ts → providers/claude/ClaudeSessionLocator.ts} +97 -239
- package/src/{utils → providers/claude}/ClaudeSessionParser.ts +2 -2
- package/src/{durable → providers/claude/durable}/ClaudeCliProbe.ts +4 -10
- package/src/providers/claude/durable/ClaudePrintAgentService.ts +66 -0
- package/src/providers/claude/durable/ClaudePrintRunner.ts +92 -0
- package/src/providers/claude/durable/ClaudeStreamParser.ts +79 -0
- package/src/providers/codex/durable/CodexCliProbe.ts +60 -0
- package/src/providers/codex/durable/CodexPrintAgentService.ts +65 -0
- package/src/providers/codex/durable/CodexPrintRunner.ts +94 -0
- package/src/providers/codex/durable/CodexStreamParser.ts +104 -0
- package/src/providers/pi/durable/PiCliProbe.ts +48 -0
- package/src/providers/pi/durable/PiPrintAgentService.ts +69 -0
- package/src/providers/pi/durable/PiPrintProtocol.ts +39 -0
- package/src/providers/pi/durable/PiPrintRunner.ts +88 -0
- package/src/providers/pi/durable/PiStreamParser.ts +84 -0
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/ClaudeCodeAdapter.js.map +0 -1
- package/dist/durable/ClaudeCliProbe.d.ts.map +0 -1
- package/dist/durable/ClaudeCliProbe.js.map +0 -1
- package/dist/durable/ClaudePrintAgentService.d.ts.map +0 -1
- package/dist/durable/ClaudePrintAgentService.js +0 -66
- package/dist/durable/ClaudePrintAgentService.js.map +0 -1
- package/dist/durable/ClaudePrintRunner.d.ts.map +0 -1
- package/dist/durable/ClaudePrintRunner.js +0 -128
- package/dist/durable/ClaudePrintRunner.js.map +0 -1
- package/dist/utils/ClaudeSessionParser.d.ts.map +0 -1
- package/dist/utils/ClaudeSessionParser.js.map +0 -1
- package/src/durable/ClaudePrintAgentService.ts +0 -94
- package/src/durable/ClaudePrintRunner.ts +0 -139
- /package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.d.ts +0 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { AgentStatus } from '../../../adapters/AgentAdapter.js';
|
|
6
|
+
import { ClaudeSessionLocator } from '../../../providers/claude/ClaudeSessionLocator.js';
|
|
7
|
+
import { batchGetSessionFileBirthtimes } from '../../../utils/session.js';
|
|
8
|
+
vi.mock('../../../utils/session.js', async (importOriginal)=>{
|
|
9
|
+
const actual = await importOriginal();
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
batchGetSessionFileBirthtimes: vi.fn()
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes;
|
|
16
|
+
const tmpDirs = [];
|
|
17
|
+
function makeTmpDir() {
|
|
18
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-locator-test-'));
|
|
19
|
+
tmpDirs.push(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
function makeProcess(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
pid: 123,
|
|
25
|
+
command: 'claude',
|
|
26
|
+
cwd: '/repo/my-app',
|
|
27
|
+
tty: 'ttys001',
|
|
28
|
+
startTime: new Date('2026-08-23T10:00:00.000Z'),
|
|
29
|
+
...overrides
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** `tryPidFileMatching` is private; tests drive it directly like the sibling adapter suites do. */ function bindPidFileMatching(locator) {
|
|
33
|
+
return locator.tryPidFileMatching.bind(locator);
|
|
34
|
+
}
|
|
35
|
+
/** `getProjectDir` is private; bound here so the encoding tests read cleanly. */ function bindProjectDir(locator) {
|
|
36
|
+
return locator.getProjectDir.bind(locator);
|
|
37
|
+
}
|
|
38
|
+
describe('ClaudeSessionLocator', ()=>{
|
|
39
|
+
beforeEach(()=>{
|
|
40
|
+
mockedBatchGetSessionFileBirthtimes.mockReset();
|
|
41
|
+
});
|
|
42
|
+
afterEach(()=>{
|
|
43
|
+
for (const dir of tmpDirs.splice(0)){
|
|
44
|
+
fs.rmSync(dir, {
|
|
45
|
+
recursive: true,
|
|
46
|
+
force: true
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
it('matches resumed sessions directly and carries live PID status metadata', ()=>{
|
|
51
|
+
const root = makeTmpDir();
|
|
52
|
+
const projectsDir = path.join(root, 'projects');
|
|
53
|
+
const sessionsDir = path.join(root, 'sessions');
|
|
54
|
+
const cwd = '/repo/my-app';
|
|
55
|
+
const sessionId = '12345678-1234-1234-1234-123456789abc';
|
|
56
|
+
const projectDir = path.join(projectsDir, '-repo-my-app');
|
|
57
|
+
const sessionFile = path.join(projectDir, `${sessionId}.jsonl`);
|
|
58
|
+
fs.mkdirSync(projectDir, {
|
|
59
|
+
recursive: true
|
|
60
|
+
});
|
|
61
|
+
fs.mkdirSync(sessionsDir, {
|
|
62
|
+
recursive: true
|
|
63
|
+
});
|
|
64
|
+
fs.writeFileSync(sessionFile, '{}\n');
|
|
65
|
+
fs.writeFileSync(path.join(sessionsDir, '123.json'), JSON.stringify({
|
|
66
|
+
pid: 123,
|
|
67
|
+
sessionId,
|
|
68
|
+
cwd,
|
|
69
|
+
startedAt: new Date('2026-08-23T10:00:00.000Z').getTime(),
|
|
70
|
+
kind: 'interactive',
|
|
71
|
+
entrypoint: 'cli',
|
|
72
|
+
status: 'waiting',
|
|
73
|
+
waitingFor: 'approve Read'
|
|
74
|
+
}));
|
|
75
|
+
const locator = new ClaudeSessionLocator({
|
|
76
|
+
projectsDir,
|
|
77
|
+
sessionsDir
|
|
78
|
+
});
|
|
79
|
+
const matches = locator.matchRunningProcesses([
|
|
80
|
+
makeProcess({
|
|
81
|
+
command: `claude --resume ${sessionId}`,
|
|
82
|
+
cwd
|
|
83
|
+
})
|
|
84
|
+
]);
|
|
85
|
+
expect(matches.direct).toHaveLength(1);
|
|
86
|
+
expect(matches.legacyMatches).toEqual([]);
|
|
87
|
+
expect(matches.direct[0]).toMatchObject({
|
|
88
|
+
pidStatus: AgentStatus.WAITING,
|
|
89
|
+
waitingFor: 'approve Read',
|
|
90
|
+
sessionFile: {
|
|
91
|
+
sessionId,
|
|
92
|
+
filePath: sessionFile,
|
|
93
|
+
projectDir,
|
|
94
|
+
resolvedCwd: cwd
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('discoverLiveSessions', ()=>{
|
|
99
|
+
let tmpDir;
|
|
100
|
+
let projectsDir;
|
|
101
|
+
let locator;
|
|
102
|
+
beforeEach(()=>{
|
|
103
|
+
tmpDir = makeTmpDir();
|
|
104
|
+
projectsDir = path.join(tmpDir, 'projects');
|
|
105
|
+
locator = new ClaudeSessionLocator({
|
|
106
|
+
projectsDir,
|
|
107
|
+
sessionsDir: path.join(tmpDir, 'sessions')
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
it('should return empty when projects dir does not exist', ()=>{
|
|
111
|
+
const missing = new ClaudeSessionLocator({
|
|
112
|
+
projectsDir: path.join(tmpDir, 'nonexistent'),
|
|
113
|
+
sessionsDir: path.join(tmpDir, 'sessions')
|
|
114
|
+
});
|
|
115
|
+
const result = missing.discoverLiveSessions([
|
|
116
|
+
{
|
|
117
|
+
pid: 1,
|
|
118
|
+
command: 'claude',
|
|
119
|
+
cwd: '/test',
|
|
120
|
+
tty: ''
|
|
121
|
+
}
|
|
122
|
+
]);
|
|
123
|
+
expect(result).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
it('should scan only directories matching process CWDs', ()=>{
|
|
126
|
+
// /my/project → -my-project (encoded dir)
|
|
127
|
+
const encodedDir = path.join(projectsDir, '-my-project');
|
|
128
|
+
fs.mkdirSync(encodedDir, {
|
|
129
|
+
recursive: true
|
|
130
|
+
});
|
|
131
|
+
// Also create another dir that should NOT be scanned
|
|
132
|
+
const otherDir = path.join(projectsDir, '-other-project');
|
|
133
|
+
fs.mkdirSync(otherDir, {
|
|
134
|
+
recursive: true
|
|
135
|
+
});
|
|
136
|
+
const mockFiles = [
|
|
137
|
+
{
|
|
138
|
+
sessionId: 's1',
|
|
139
|
+
filePath: path.join(encodedDir, 's1.jsonl'),
|
|
140
|
+
projectDir: encodedDir,
|
|
141
|
+
birthtimeMs: 1710800324000,
|
|
142
|
+
resolvedCwd: ''
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue(mockFiles);
|
|
146
|
+
const processes = [
|
|
147
|
+
{
|
|
148
|
+
pid: 1,
|
|
149
|
+
command: 'claude',
|
|
150
|
+
cwd: '/my/project',
|
|
151
|
+
tty: ''
|
|
152
|
+
}
|
|
153
|
+
];
|
|
154
|
+
const result = locator.discoverLiveSessions(processes);
|
|
155
|
+
expect(result).toHaveLength(1);
|
|
156
|
+
expect(result[0].resolvedCwd).toBe('/my/project');
|
|
157
|
+
// batchGetSessionFileBirthtimes called once with all dirs
|
|
158
|
+
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledWith([
|
|
160
|
+
encodedDir
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
it('should return empty when encoded dir does not exist', ()=>{
|
|
164
|
+
fs.mkdirSync(projectsDir, {
|
|
165
|
+
recursive: true
|
|
166
|
+
});
|
|
167
|
+
// Process CWD /test encodes to -test, but that dir doesn't exist
|
|
168
|
+
const result = locator.discoverLiveSessions([
|
|
169
|
+
{
|
|
170
|
+
pid: 1,
|
|
171
|
+
command: 'claude',
|
|
172
|
+
cwd: '/test',
|
|
173
|
+
tty: ''
|
|
174
|
+
}
|
|
175
|
+
]);
|
|
176
|
+
expect(result).toEqual([]);
|
|
177
|
+
expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
it('should deduplicate when multiple processes share same CWD', ()=>{
|
|
180
|
+
const encodedDir = path.join(projectsDir, '-my-project');
|
|
181
|
+
fs.mkdirSync(encodedDir, {
|
|
182
|
+
recursive: true
|
|
183
|
+
});
|
|
184
|
+
mockedBatchGetSessionFileBirthtimes.mockReturnValue([
|
|
185
|
+
{
|
|
186
|
+
sessionId: 's1',
|
|
187
|
+
filePath: path.join(encodedDir, 's1.jsonl'),
|
|
188
|
+
projectDir: encodedDir,
|
|
189
|
+
birthtimeMs: 1710800324000,
|
|
190
|
+
resolvedCwd: ''
|
|
191
|
+
}
|
|
192
|
+
]);
|
|
193
|
+
const processes = [
|
|
194
|
+
{
|
|
195
|
+
pid: 1,
|
|
196
|
+
command: 'claude',
|
|
197
|
+
cwd: '/my/project',
|
|
198
|
+
tty: ''
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
pid: 2,
|
|
202
|
+
command: 'claude',
|
|
203
|
+
cwd: '/my/project',
|
|
204
|
+
tty: ''
|
|
205
|
+
}
|
|
206
|
+
];
|
|
207
|
+
const result = locator.discoverLiveSessions(processes);
|
|
208
|
+
// Should only call batch once with deduplicated dir
|
|
209
|
+
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);
|
|
210
|
+
expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledWith([
|
|
211
|
+
encodedDir
|
|
212
|
+
]);
|
|
213
|
+
expect(result).toHaveLength(1);
|
|
214
|
+
});
|
|
215
|
+
it('should skip processes with empty cwd', ()=>{
|
|
216
|
+
fs.mkdirSync(projectsDir, {
|
|
217
|
+
recursive: true
|
|
218
|
+
});
|
|
219
|
+
const result = locator.discoverLiveSessions([
|
|
220
|
+
{
|
|
221
|
+
pid: 1,
|
|
222
|
+
command: 'claude',
|
|
223
|
+
cwd: '',
|
|
224
|
+
tty: ''
|
|
225
|
+
}
|
|
226
|
+
]);
|
|
227
|
+
expect(result).toEqual([]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe('tryPidFileMatching', ()=>{
|
|
231
|
+
let tmpDir;
|
|
232
|
+
let sessionsDir;
|
|
233
|
+
let projectsDir;
|
|
234
|
+
let tryMatch;
|
|
235
|
+
beforeEach(()=>{
|
|
236
|
+
tmpDir = makeTmpDir();
|
|
237
|
+
sessionsDir = path.join(tmpDir, 'sessions');
|
|
238
|
+
projectsDir = path.join(tmpDir, 'projects');
|
|
239
|
+
fs.mkdirSync(sessionsDir, {
|
|
240
|
+
recursive: true
|
|
241
|
+
});
|
|
242
|
+
tryMatch = bindPidFileMatching(new ClaudeSessionLocator({
|
|
243
|
+
projectsDir,
|
|
244
|
+
sessionsDir
|
|
245
|
+
}));
|
|
246
|
+
});
|
|
247
|
+
const makeProc = (pid, cwd = '/project/test', startTime)=>({
|
|
248
|
+
pid,
|
|
249
|
+
command: 'claude',
|
|
250
|
+
cwd,
|
|
251
|
+
tty: 'ttys001',
|
|
252
|
+
startTime
|
|
253
|
+
});
|
|
254
|
+
const writePidFile = (pid, sessionId, cwd, startedAt)=>{
|
|
255
|
+
fs.writeFileSync(path.join(sessionsDir, `${pid}.json`), JSON.stringify({
|
|
256
|
+
pid,
|
|
257
|
+
sessionId,
|
|
258
|
+
cwd,
|
|
259
|
+
startedAt,
|
|
260
|
+
kind: 'interactive',
|
|
261
|
+
entrypoint: 'cli'
|
|
262
|
+
}));
|
|
263
|
+
};
|
|
264
|
+
const writeJsonl = (cwd, sessionId)=>{
|
|
265
|
+
const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
|
|
266
|
+
const projDir = path.join(projectsDir, encoded);
|
|
267
|
+
fs.mkdirSync(projDir, {
|
|
268
|
+
recursive: true
|
|
269
|
+
});
|
|
270
|
+
const filePath = path.join(projDir, `${sessionId}.jsonl`);
|
|
271
|
+
fs.writeFileSync(filePath, JSON.stringify({
|
|
272
|
+
type: 'assistant',
|
|
273
|
+
timestamp: new Date().toISOString()
|
|
274
|
+
}));
|
|
275
|
+
return filePath;
|
|
276
|
+
};
|
|
277
|
+
it('should return direct match when PID file and JSONL both exist within time tolerance', ()=>{
|
|
278
|
+
const startTime = new Date();
|
|
279
|
+
const proc = makeProc(1001, '/project/test', startTime);
|
|
280
|
+
writePidFile(1001, 'session-abc', '/project/test', startTime.getTime());
|
|
281
|
+
writeJsonl('/project/test', 'session-abc');
|
|
282
|
+
const { direct, fallback } = tryMatch([
|
|
283
|
+
proc
|
|
284
|
+
]);
|
|
285
|
+
expect(direct).toHaveLength(1);
|
|
286
|
+
expect(fallback).toHaveLength(0);
|
|
287
|
+
expect(direct[0].sessionFile.sessionId).toBe('session-abc');
|
|
288
|
+
expect(direct[0].sessionFile.resolvedCwd).toBe('/project/test');
|
|
289
|
+
expect(direct[0].process.pid).toBe(1001);
|
|
290
|
+
});
|
|
291
|
+
it('should fall back when PID file exists but JSONL is missing', ()=>{
|
|
292
|
+
const startTime = new Date();
|
|
293
|
+
const proc = makeProc(1002, '/project/test', startTime);
|
|
294
|
+
writePidFile(1002, 'nonexistent-session', '/project/test', startTime.getTime());
|
|
295
|
+
// No JSONL file written
|
|
296
|
+
const { direct, fallback } = tryMatch([
|
|
297
|
+
proc
|
|
298
|
+
]);
|
|
299
|
+
expect(direct).toHaveLength(0);
|
|
300
|
+
expect(fallback).toHaveLength(1);
|
|
301
|
+
expect(fallback[0].pid).toBe(1002);
|
|
302
|
+
});
|
|
303
|
+
it('should fall back when startedAt is stale (>60s from proc.startTime)', ()=>{
|
|
304
|
+
const startTime = new Date();
|
|
305
|
+
const staleTime = startTime.getTime() - 90_000; // 90 seconds earlier
|
|
306
|
+
const proc = makeProc(1003, '/project/test', startTime);
|
|
307
|
+
writePidFile(1003, 'stale-session', '/project/test', staleTime);
|
|
308
|
+
writeJsonl('/project/test', 'stale-session');
|
|
309
|
+
const { direct, fallback } = tryMatch([
|
|
310
|
+
proc
|
|
311
|
+
]);
|
|
312
|
+
expect(direct).toHaveLength(0);
|
|
313
|
+
expect(fallback).toHaveLength(1);
|
|
314
|
+
});
|
|
315
|
+
it('should accept PID file when startedAt is within 60s tolerance', ()=>{
|
|
316
|
+
const startTime = new Date();
|
|
317
|
+
const closeTime = startTime.getTime() - 30_000; // 30 seconds earlier — within tolerance
|
|
318
|
+
const proc = makeProc(1004, '/project/test', startTime);
|
|
319
|
+
writePidFile(1004, 'close-session', '/project/test', closeTime);
|
|
320
|
+
writeJsonl('/project/test', 'close-session');
|
|
321
|
+
const { direct, fallback } = tryMatch([
|
|
322
|
+
proc
|
|
323
|
+
]);
|
|
324
|
+
expect(direct).toHaveLength(1);
|
|
325
|
+
expect(fallback).toHaveLength(0);
|
|
326
|
+
});
|
|
327
|
+
it('should fall back when PID file is absent', ()=>{
|
|
328
|
+
const proc = makeProc(1005, '/project/test', new Date());
|
|
329
|
+
// No PID file written
|
|
330
|
+
const { direct, fallback } = tryMatch([
|
|
331
|
+
proc
|
|
332
|
+
]);
|
|
333
|
+
expect(direct).toHaveLength(0);
|
|
334
|
+
expect(fallback).toHaveLength(1);
|
|
335
|
+
});
|
|
336
|
+
it('should fall back when PID file contains malformed JSON', ()=>{
|
|
337
|
+
const proc = makeProc(1006, '/project/test', new Date());
|
|
338
|
+
fs.writeFileSync(path.join(sessionsDir, '1006.json'), 'not valid json {{{');
|
|
339
|
+
expect(()=>{
|
|
340
|
+
const { direct, fallback } = tryMatch([
|
|
341
|
+
proc
|
|
342
|
+
]);
|
|
343
|
+
expect(direct).toHaveLength(0);
|
|
344
|
+
expect(fallback).toHaveLength(1);
|
|
345
|
+
}).not.toThrow();
|
|
346
|
+
});
|
|
347
|
+
it('should fall back for all processes when sessions dir does not exist', ()=>{
|
|
348
|
+
const missingSessions = bindPidFileMatching(new ClaudeSessionLocator({
|
|
349
|
+
projectsDir,
|
|
350
|
+
sessionsDir: path.join(tmpDir, 'nonexistent-sessions')
|
|
351
|
+
}));
|
|
352
|
+
const processes = [
|
|
353
|
+
makeProc(2001, '/a', new Date()),
|
|
354
|
+
makeProc(2002, '/b', new Date())
|
|
355
|
+
];
|
|
356
|
+
const { direct, fallback } = missingSessions(processes);
|
|
357
|
+
expect(direct).toHaveLength(0);
|
|
358
|
+
expect(fallback).toHaveLength(2);
|
|
359
|
+
});
|
|
360
|
+
it('should correctly split mixed processes (some with PID files, some without)', ()=>{
|
|
361
|
+
const startTime = new Date();
|
|
362
|
+
const proc1 = makeProc(3001, '/project/one', startTime);
|
|
363
|
+
const proc2 = makeProc(3002, '/project/two', startTime);
|
|
364
|
+
const proc3 = makeProc(3003, '/project/three', startTime);
|
|
365
|
+
writePidFile(3001, 'session-one', '/project/one', startTime.getTime());
|
|
366
|
+
writeJsonl('/project/one', 'session-one');
|
|
367
|
+
writePidFile(3003, 'session-three', '/project/three', startTime.getTime());
|
|
368
|
+
writeJsonl('/project/three', 'session-three');
|
|
369
|
+
// proc2 has no PID file
|
|
370
|
+
const { direct, fallback } = tryMatch([
|
|
371
|
+
proc1,
|
|
372
|
+
proc2,
|
|
373
|
+
proc3
|
|
374
|
+
]);
|
|
375
|
+
expect(direct).toHaveLength(2);
|
|
376
|
+
expect(fallback).toHaveLength(1);
|
|
377
|
+
expect(direct.map((d)=>d.process.pid).sort()).toEqual([
|
|
378
|
+
3001,
|
|
379
|
+
3003
|
|
380
|
+
]);
|
|
381
|
+
expect(fallback[0].pid).toBe(3002);
|
|
382
|
+
});
|
|
383
|
+
it('should skip stale-file check when proc.startTime is undefined', ()=>{
|
|
384
|
+
const proc = makeProc(4001, '/project/test', undefined); // no startTime
|
|
385
|
+
writePidFile(4001, 'no-time-session', '/project/test', Date.now() - 999_999);
|
|
386
|
+
writeJsonl('/project/test', 'no-time-session');
|
|
387
|
+
const { direct, fallback } = tryMatch([
|
|
388
|
+
proc
|
|
389
|
+
]);
|
|
390
|
+
// startTime undefined → stale check skipped → direct match
|
|
391
|
+
expect(direct).toHaveLength(1);
|
|
392
|
+
expect(fallback).toHaveLength(0);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
describe('getProjectDir', ()=>{
|
|
396
|
+
let tmpDir;
|
|
397
|
+
let projectsDir;
|
|
398
|
+
let encode;
|
|
399
|
+
beforeEach(()=>{
|
|
400
|
+
tmpDir = makeTmpDir();
|
|
401
|
+
projectsDir = path.join(tmpDir, 'projects');
|
|
402
|
+
encode = bindProjectDir(new ClaudeSessionLocator({
|
|
403
|
+
projectsDir,
|
|
404
|
+
sessionsDir: path.join(tmpDir, 'sessions')
|
|
405
|
+
}));
|
|
406
|
+
});
|
|
407
|
+
it('should replace path separators with hyphens', ()=>{
|
|
408
|
+
expect(encode('/Users/foo/bar')).toBe(path.join(projectsDir, '-Users-foo-bar'));
|
|
409
|
+
});
|
|
410
|
+
it('should encode underscores as hyphens (matches Claude Code CLI)', ()=>{
|
|
411
|
+
expect(encode('/Users/foo/my_project')).toBe(path.join(projectsDir, '-Users-foo-my-project'));
|
|
412
|
+
});
|
|
413
|
+
it('should encode dots as hyphens', ()=>{
|
|
414
|
+
expect(encode('/Users/foo/.worktrees/x')).toBe(path.join(projectsDir, '-Users-foo--worktrees-x'));
|
|
415
|
+
});
|
|
416
|
+
it('should collide paths that differ only in non-alphanumeric chars', ()=>{
|
|
417
|
+
// The encoding is intentionally lossy — callers must
|
|
418
|
+
// disambiguate via session JSONL contents, not dir name.
|
|
419
|
+
expect(encode('/a/b_c')).toBe(encode('/a/b-c'));
|
|
420
|
+
expect(encode('/a/b_c')).toBe(encode('/a/b.c'));
|
|
421
|
+
});
|
|
422
|
+
it('should resolve to a real session dir when cwd contains underscores', ()=>{
|
|
423
|
+
const cwd = '/Users/foo/my_project';
|
|
424
|
+
const expectedDir = path.join(projectsDir, '-Users-foo-my-project');
|
|
425
|
+
fs.mkdirSync(expectedDir, {
|
|
426
|
+
recursive: true
|
|
427
|
+
});
|
|
428
|
+
const sessionFile = path.join(expectedDir, 'session-underscore.jsonl');
|
|
429
|
+
fs.writeFileSync(sessionFile, '');
|
|
430
|
+
expect(encode(cwd)).toBe(expectedDir);
|
|
431
|
+
expect(fs.existsSync(path.join(encode(cwd), 'session-underscore.jsonl'))).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
//# sourceMappingURL=ClaudeSessionLocator.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/__tests__/providers/claude/ClaudeSessionLocator.test.ts"],"sourcesContent":["import type { MockedFunction } from 'vitest';\nimport * as fs from 'fs';\nimport * as os from 'os';\nimport * as path from 'path';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { AgentStatus, type ProcessInfo } from '../../../adapters/AgentAdapter.js';\nimport { ClaudeSessionLocator } from '../../../providers/claude/ClaudeSessionLocator.js';\nimport { batchGetSessionFileBirthtimes } from '../../../utils/session.js';\nimport type { SessionFile } from '../../../utils/session.js';\n\nvi.mock('../../../utils/session.js', async (importOriginal) => {\n const actual = await importOriginal() as typeof import('../../../utils/session.js');\n return {\n ...actual,\n batchGetSessionFileBirthtimes: vi.fn(),\n };\n});\n\nconst mockedBatchGetSessionFileBirthtimes =\n batchGetSessionFileBirthtimes as MockedFunction<typeof batchGetSessionFileBirthtimes>;\n\nconst tmpDirs: string[] = [];\n\nfunction makeTmpDir(): string {\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-locator-test-'));\n tmpDirs.push(dir);\n return dir;\n}\n\nfunction makeProcess(overrides: Partial<ProcessInfo> = {}): ProcessInfo {\n return {\n pid: 123,\n command: 'claude',\n cwd: '/repo/my-app',\n tty: 'ttys001',\n startTime: new Date('2026-08-23T10:00:00.000Z'),\n ...overrides,\n };\n}\n\ntype PidFileMatching = (processes: ProcessInfo[]) => {\n direct: Array<{ process: ProcessInfo; sessionFile: SessionFile }>;\n fallback: ProcessInfo[];\n};\n\n/** `tryPidFileMatching` is private; tests drive it directly like the sibling adapter suites do. */\nfunction bindPidFileMatching(locator: ClaudeSessionLocator): PidFileMatching {\n return (locator as unknown as { tryPidFileMatching: PidFileMatching })\n .tryPidFileMatching.bind(locator) as PidFileMatching;\n}\n\n/** `getProjectDir` is private; bound here so the encoding tests read cleanly. */\nfunction bindProjectDir(locator: ClaudeSessionLocator): (cwd: string) => string {\n return (locator as unknown as { getProjectDir: (cwd: string) => string })\n .getProjectDir.bind(locator);\n}\n\ndescribe('ClaudeSessionLocator', () => {\n beforeEach(() => {\n mockedBatchGetSessionFileBirthtimes.mockReset();\n });\n\n afterEach(() => {\n for (const dir of tmpDirs.splice(0)) {\n fs.rmSync(dir, { recursive: true, force: true });\n }\n });\n\n it('matches resumed sessions directly and carries live PID status metadata', () => {\n const root = makeTmpDir();\n const projectsDir = path.join(root, 'projects');\n const sessionsDir = path.join(root, 'sessions');\n const cwd = '/repo/my-app';\n const sessionId = '12345678-1234-1234-1234-123456789abc';\n const projectDir = path.join(projectsDir, '-repo-my-app');\n const sessionFile = path.join(projectDir, `${sessionId}.jsonl`);\n fs.mkdirSync(projectDir, { recursive: true });\n fs.mkdirSync(sessionsDir, { recursive: true });\n fs.writeFileSync(sessionFile, '{}\\n');\n fs.writeFileSync(path.join(sessionsDir, '123.json'), JSON.stringify({\n pid: 123,\n sessionId,\n cwd,\n startedAt: new Date('2026-08-23T10:00:00.000Z').getTime(),\n kind: 'interactive',\n entrypoint: 'cli',\n status: 'waiting',\n waitingFor: 'approve Read',\n }));\n\n const locator = new ClaudeSessionLocator({ projectsDir, sessionsDir });\n const matches = locator.matchRunningProcesses([\n makeProcess({ command: `claude --resume ${sessionId}`, cwd }),\n ]);\n\n expect(matches.direct).toHaveLength(1);\n expect(matches.legacyMatches).toEqual([]);\n expect(matches.direct[0]).toMatchObject({\n pidStatus: AgentStatus.WAITING,\n waitingFor: 'approve Read',\n sessionFile: {\n sessionId,\n filePath: sessionFile,\n projectDir,\n resolvedCwd: cwd,\n },\n });\n });\n describe('discoverLiveSessions', () => {\n let tmpDir: string;\n let projectsDir: string;\n let locator: ClaudeSessionLocator;\n\n beforeEach(() => {\n tmpDir = makeTmpDir();\n projectsDir = path.join(tmpDir, 'projects');\n locator = new ClaudeSessionLocator({\n projectsDir,\n sessionsDir: path.join(tmpDir, 'sessions'),\n });\n });\n\n it('should return empty when projects dir does not exist', () => {\n const missing = new ClaudeSessionLocator({\n projectsDir: path.join(tmpDir, 'nonexistent'),\n sessionsDir: path.join(tmpDir, 'sessions'),\n });\n\n const result = missing.discoverLiveSessions([\n { pid: 1, command: 'claude', cwd: '/test', tty: '' },\n ]);\n expect(result).toEqual([]);\n });\n\n it('should scan only directories matching process CWDs', () => {\n // /my/project → -my-project (encoded dir)\n const encodedDir = path.join(projectsDir, '-my-project');\n fs.mkdirSync(encodedDir, { recursive: true });\n\n // Also create another dir that should NOT be scanned\n const otherDir = path.join(projectsDir, '-other-project');\n fs.mkdirSync(otherDir, { recursive: true });\n\n const mockFiles: SessionFile[] = [\n {\n sessionId: 's1',\n filePath: path.join(encodedDir, 's1.jsonl'),\n projectDir: encodedDir,\n birthtimeMs: 1710800324000,\n resolvedCwd: '',\n },\n ];\n mockedBatchGetSessionFileBirthtimes.mockReturnValue(mockFiles);\n\n const processes = [\n { pid: 1, command: 'claude', cwd: '/my/project', tty: '' },\n ];\n\n const result = locator.discoverLiveSessions(processes);\n expect(result).toHaveLength(1);\n expect(result[0].resolvedCwd).toBe('/my/project');\n // batchGetSessionFileBirthtimes called once with all dirs\n expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);\n expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledWith([encodedDir]);\n });\n\n it('should return empty when encoded dir does not exist', () => {\n fs.mkdirSync(projectsDir, { recursive: true });\n\n // Process CWD /test encodes to -test, but that dir doesn't exist\n const result = locator.discoverLiveSessions([\n { pid: 1, command: 'claude', cwd: '/test', tty: '' },\n ]);\n expect(result).toEqual([]);\n expect(mockedBatchGetSessionFileBirthtimes).not.toHaveBeenCalled();\n });\n\n it('should deduplicate when multiple processes share same CWD', () => {\n const encodedDir = path.join(projectsDir, '-my-project');\n fs.mkdirSync(encodedDir, { recursive: true });\n\n mockedBatchGetSessionFileBirthtimes.mockReturnValue([\n { sessionId: 's1', filePath: path.join(encodedDir, 's1.jsonl'), projectDir: encodedDir, birthtimeMs: 1710800324000, resolvedCwd: '' },\n ]);\n\n const processes = [\n { pid: 1, command: 'claude', cwd: '/my/project', tty: '' },\n { pid: 2, command: 'claude', cwd: '/my/project', tty: '' },\n ];\n\n const result = locator.discoverLiveSessions(processes);\n // Should only call batch once with deduplicated dir\n expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledTimes(1);\n expect(mockedBatchGetSessionFileBirthtimes).toHaveBeenCalledWith([encodedDir]);\n expect(result).toHaveLength(1);\n });\n\n it('should skip processes with empty cwd', () => {\n fs.mkdirSync(projectsDir, { recursive: true });\n\n const result = locator.discoverLiveSessions([\n { pid: 1, command: 'claude', cwd: '', tty: '' },\n ]);\n expect(result).toEqual([]);\n });\n });\n\n describe('tryPidFileMatching', () => {\n let tmpDir: string;\n let sessionsDir: string;\n let projectsDir: string;\n let tryMatch: (processes: ProcessInfo[]) => {\n direct: Array<{ process: ProcessInfo; sessionFile: SessionFile }>;\n fallback: ProcessInfo[];\n };\n\n beforeEach(() => {\n tmpDir = makeTmpDir();\n sessionsDir = path.join(tmpDir, 'sessions');\n projectsDir = path.join(tmpDir, 'projects');\n fs.mkdirSync(sessionsDir, { recursive: true });\n tryMatch = bindPidFileMatching(new ClaudeSessionLocator({ projectsDir, sessionsDir }));\n });\n\n const makeProc = (pid: number, cwd = '/project/test', startTime?: Date): ProcessInfo => ({\n pid, command: 'claude', cwd, tty: 'ttys001', startTime,\n });\n\n const writePidFile = (pid: number, sessionId: string, cwd: string, startedAt: number) => {\n fs.writeFileSync(\n path.join(sessionsDir, `${pid}.json`),\n JSON.stringify({ pid, sessionId, cwd, startedAt, kind: 'interactive', entrypoint: 'cli' }),\n );\n };\n\n const writeJsonl = (cwd: string, sessionId: string) => {\n const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');\n const projDir = path.join(projectsDir, encoded);\n fs.mkdirSync(projDir, { recursive: true });\n const filePath = path.join(projDir, `${sessionId}.jsonl`);\n fs.writeFileSync(filePath, JSON.stringify({ type: 'assistant', timestamp: new Date().toISOString() }));\n return filePath;\n };\n\n it('should return direct match when PID file and JSONL both exist within time tolerance', () => {\n const startTime = new Date();\n const proc = makeProc(1001, '/project/test', startTime);\n writePidFile(1001, 'session-abc', '/project/test', startTime.getTime());\n writeJsonl('/project/test', 'session-abc');\n\n const { direct, fallback } = tryMatch([proc]);\n\n expect(direct).toHaveLength(1);\n expect(fallback).toHaveLength(0);\n expect(direct[0].sessionFile.sessionId).toBe('session-abc');\n expect(direct[0].sessionFile.resolvedCwd).toBe('/project/test');\n expect(direct[0].process.pid).toBe(1001);\n });\n\n it('should fall back when PID file exists but JSONL is missing', () => {\n const startTime = new Date();\n const proc = makeProc(1002, '/project/test', startTime);\n writePidFile(1002, 'nonexistent-session', '/project/test', startTime.getTime());\n // No JSONL file written\n\n const { direct, fallback } = tryMatch([proc]);\n\n expect(direct).toHaveLength(0);\n expect(fallback).toHaveLength(1);\n expect(fallback[0].pid).toBe(1002);\n });\n\n it('should fall back when startedAt is stale (>60s from proc.startTime)', () => {\n const startTime = new Date();\n const staleTime = startTime.getTime() - 90_000; // 90 seconds earlier\n const proc = makeProc(1003, '/project/test', startTime);\n writePidFile(1003, 'stale-session', '/project/test', staleTime);\n writeJsonl('/project/test', 'stale-session');\n\n const { direct, fallback } = tryMatch([proc]);\n\n expect(direct).toHaveLength(0);\n expect(fallback).toHaveLength(1);\n });\n\n it('should accept PID file when startedAt is within 60s tolerance', () => {\n const startTime = new Date();\n const closeTime = startTime.getTime() - 30_000; // 30 seconds earlier — within tolerance\n const proc = makeProc(1004, '/project/test', startTime);\n writePidFile(1004, 'close-session', '/project/test', closeTime);\n writeJsonl('/project/test', 'close-session');\n\n const { direct, fallback } = tryMatch([proc]);\n\n expect(direct).toHaveLength(1);\n expect(fallback).toHaveLength(0);\n });\n\n it('should fall back when PID file is absent', () => {\n const proc = makeProc(1005, '/project/test', new Date());\n // No PID file written\n\n const { direct, fallback } = tryMatch([proc]);\n\n expect(direct).toHaveLength(0);\n expect(fallback).toHaveLength(1);\n });\n\n it('should fall back when PID file contains malformed JSON', () => {\n const proc = makeProc(1006, '/project/test', new Date());\n fs.writeFileSync(path.join(sessionsDir, '1006.json'), 'not valid json {{{');\n\n expect(() => {\n const { direct, fallback } = tryMatch([proc]);\n expect(direct).toHaveLength(0);\n expect(fallback).toHaveLength(1);\n }).not.toThrow();\n });\n\n it('should fall back for all processes when sessions dir does not exist', () => {\n const missingSessions = bindPidFileMatching(new ClaudeSessionLocator({\n projectsDir,\n sessionsDir: path.join(tmpDir, 'nonexistent-sessions'),\n }));\n const processes = [makeProc(2001, '/a', new Date()), makeProc(2002, '/b', new Date())];\n\n const { direct, fallback } = missingSessions(processes);\n\n expect(direct).toHaveLength(0);\n expect(fallback).toHaveLength(2);\n });\n\n it('should correctly split mixed processes (some with PID files, some without)', () => {\n const startTime = new Date();\n const proc1 = makeProc(3001, '/project/one', startTime);\n const proc2 = makeProc(3002, '/project/two', startTime);\n const proc3 = makeProc(3003, '/project/three', startTime);\n\n writePidFile(3001, 'session-one', '/project/one', startTime.getTime());\n writeJsonl('/project/one', 'session-one');\n writePidFile(3003, 'session-three', '/project/three', startTime.getTime());\n writeJsonl('/project/three', 'session-three');\n // proc2 has no PID file\n\n const { direct, fallback } = tryMatch([proc1, proc2, proc3]);\n\n expect(direct).toHaveLength(2);\n expect(fallback).toHaveLength(1);\n expect(direct.map((d) => d.process.pid).sort()).toEqual([3001, 3003]);\n expect(fallback[0].pid).toBe(3002);\n });\n\n it('should skip stale-file check when proc.startTime is undefined', () => {\n const proc = makeProc(4001, '/project/test', undefined); // no startTime\n writePidFile(4001, 'no-time-session', '/project/test', Date.now() - 999_999);\n writeJsonl('/project/test', 'no-time-session');\n\n const { direct, fallback } = tryMatch([proc]);\n\n // startTime undefined → stale check skipped → direct match\n expect(direct).toHaveLength(1);\n expect(fallback).toHaveLength(0);\n });\n });\n\n describe('getProjectDir', () => {\n let tmpDir: string;\n let projectsDir: string;\n let encode: (cwd: string) => string;\n\n beforeEach(() => {\n tmpDir = makeTmpDir();\n projectsDir = path.join(tmpDir, 'projects');\n encode = bindProjectDir(new ClaudeSessionLocator({\n projectsDir,\n sessionsDir: path.join(tmpDir, 'sessions'),\n }));\n });\n\n it('should replace path separators with hyphens', () => {\n expect(encode('/Users/foo/bar')).toBe(path.join(projectsDir, '-Users-foo-bar'));\n });\n\n it('should encode underscores as hyphens (matches Claude Code CLI)', () => {\n expect(encode('/Users/foo/my_project')).toBe(path.join(projectsDir, '-Users-foo-my-project'));\n });\n\n it('should encode dots as hyphens', () => {\n expect(encode('/Users/foo/.worktrees/x')).toBe(path.join(projectsDir, '-Users-foo--worktrees-x'));\n });\n\n it('should collide paths that differ only in non-alphanumeric chars', () => {\n // The encoding is intentionally lossy — callers must\n // disambiguate via session JSONL contents, not dir name.\n expect(encode('/a/b_c')).toBe(encode('/a/b-c'));\n expect(encode('/a/b_c')).toBe(encode('/a/b.c'));\n });\n\n it('should resolve to a real session dir when cwd contains underscores', () => {\n const cwd = '/Users/foo/my_project';\n const expectedDir = path.join(projectsDir, '-Users-foo-my-project');\n fs.mkdirSync(expectedDir, { recursive: true });\n const sessionFile = path.join(expectedDir, 'session-underscore.jsonl');\n fs.writeFileSync(sessionFile, '');\n\n expect(encode(cwd)).toBe(expectedDir);\n expect(fs.existsSync(path.join(encode(cwd), 'session-underscore.jsonl'))).toBe(true);\n });\n });\n});\n"],"names":["fs","os","path","afterEach","beforeEach","describe","expect","it","vi","AgentStatus","ClaudeSessionLocator","batchGetSessionFileBirthtimes","mock","importOriginal","actual","fn","mockedBatchGetSessionFileBirthtimes","tmpDirs","makeTmpDir","dir","mkdtempSync","join","tmpdir","push","makeProcess","overrides","pid","command","cwd","tty","startTime","Date","bindPidFileMatching","locator","tryPidFileMatching","bind","bindProjectDir","getProjectDir","mockReset","splice","rmSync","recursive","force","root","projectsDir","sessionsDir","sessionId","projectDir","sessionFile","mkdirSync","writeFileSync","JSON","stringify","startedAt","getTime","kind","entrypoint","status","waitingFor","matches","matchRunningProcesses","direct","toHaveLength","legacyMatches","toEqual","toMatchObject","pidStatus","WAITING","filePath","resolvedCwd","tmpDir","missing","result","discoverLiveSessions","encodedDir","otherDir","mockFiles","birthtimeMs","mockReturnValue","processes","toBe","toHaveBeenCalledTimes","toHaveBeenCalledWith","not","toHaveBeenCalled","tryMatch","makeProc","writePidFile","writeJsonl","encoded","replace","projDir","type","timestamp","toISOString","proc","fallback","process","staleTime","closeTime","toThrow","missingSessions","proc1","proc2","proc3","map","d","sort","undefined","now","encode","expectedDir","existsSync"],"mappings":"AACA,YAAYA,QAAQ,KAAK;AACzB,YAAYC,QAAQ,KAAK;AACzB,YAAYC,UAAU,OAAO;AAC7B,SAASC,SAAS,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAEzE,SAASC,WAAW,QAA0B,oCAAoC;AAClF,SAASC,oBAAoB,QAAQ,oDAAoD;AACzF,SAASC,6BAA6B,QAAQ,4BAA4B;AAG1EH,GAAGI,IAAI,CAAC,6BAA6B,OAAOC;IACxC,MAAMC,SAAS,MAAMD;IACrB,OAAO;QACH,GAAGC,MAAM;QACTH,+BAA+BH,GAAGO,EAAE;IACxC;AACJ;AAEA,MAAMC,sCACFL;AAEJ,MAAMM,UAAoB,EAAE;AAE5B,SAASC;IACL,MAAMC,MAAMnB,GAAGoB,WAAW,CAAClB,KAAKmB,IAAI,CAACpB,GAAGqB,MAAM,IAAI;IAClDL,QAAQM,IAAI,CAACJ;IACb,OAAOA;AACX;AAEA,SAASK,YAAYC,YAAkC,CAAC,CAAC;IACrD,OAAO;QACHC,KAAK;QACLC,SAAS;QACTC,KAAK;QACLC,KAAK;QACLC,WAAW,IAAIC,KAAK;QACpB,GAAGN,SAAS;IAChB;AACJ;AAOA,iGAAiG,GACjG,SAASO,oBAAoBC,OAA6B;IACtD,OAAO,AAACA,QACHC,kBAAkB,CAACC,IAAI,CAACF;AACjC;AAEA,+EAA+E,GAC/E,SAASG,eAAeH,OAA6B;IACjD,OAAO,AAACA,QACHI,aAAa,CAACF,IAAI,CAACF;AAC5B;AAEA5B,SAAS,wBAAwB;IAC7BD,WAAW;QACPY,oCAAoCsB,SAAS;IACjD;IAEAnC,UAAU;QACN,KAAK,MAAMgB,OAAOF,QAAQsB,MAAM,CAAC,GAAI;YACjCvC,GAAGwC,MAAM,CAACrB,KAAK;gBAAEsB,WAAW;gBAAMC,OAAO;YAAK;QAClD;IACJ;IAEAnC,GAAG,0EAA0E;QACzE,MAAMoC,OAAOzB;QACb,MAAM0B,cAAc1C,KAAKmB,IAAI,CAACsB,MAAM;QACpC,MAAME,cAAc3C,KAAKmB,IAAI,CAACsB,MAAM;QACpC,MAAMf,MAAM;QACZ,MAAMkB,YAAY;QAClB,MAAMC,aAAa7C,KAAKmB,IAAI,CAACuB,aAAa;QAC1C,MAAMI,cAAc9C,KAAKmB,IAAI,CAAC0B,YAAY,GAAGD,UAAU,MAAM,CAAC;QAC9D9C,GAAGiD,SAAS,CAACF,YAAY;YAAEN,WAAW;QAAK;QAC3CzC,GAAGiD,SAAS,CAACJ,aAAa;YAAEJ,WAAW;QAAK;QAC5CzC,GAAGkD,aAAa,CAACF,aAAa;QAC9BhD,GAAGkD,aAAa,CAAChD,KAAKmB,IAAI,CAACwB,aAAa,aAAaM,KAAKC,SAAS,CAAC;YAChE1B,KAAK;YACLoB;YACAlB;YACAyB,WAAW,IAAItB,KAAK,4BAA4BuB,OAAO;YACvDC,MAAM;YACNC,YAAY;YACZC,QAAQ;YACRC,YAAY;QAChB;QAEA,MAAMzB,UAAU,IAAIvB,qBAAqB;YAAEkC;YAAaC;QAAY;QACpE,MAAMc,UAAU1B,QAAQ2B,qBAAqB,CAAC;YAC1CpC,YAAY;gBAAEG,SAAS,CAAC,gBAAgB,EAAEmB,WAAW;gBAAElB;YAAI;SAC9D;QAEDtB,OAAOqD,QAAQE,MAAM,EAAEC,YAAY,CAAC;QACpCxD,OAAOqD,QAAQI,aAAa,EAAEC,OAAO,CAAC,EAAE;QACxC1D,OAAOqD,QAAQE,MAAM,CAAC,EAAE,EAAEI,aAAa,CAAC;YACpCC,WAAWzD,YAAY0D,OAAO;YAC9BT,YAAY;YACZV,aAAa;gBACTF;gBACAsB,UAAUpB;gBACVD;gBACAsB,aAAazC;YACjB;QACJ;IACJ;IACAvB,SAAS,wBAAwB;QAC7B,IAAIiE;QACJ,IAAI1B;QACJ,IAAIX;QAEJ7B,WAAW;YACPkE,SAASpD;YACT0B,cAAc1C,KAAKmB,IAAI,CAACiD,QAAQ;YAChCrC,UAAU,IAAIvB,qBAAqB;gBAC/BkC;gBACAC,aAAa3C,KAAKmB,IAAI,CAACiD,QAAQ;YACnC;QACJ;QAEA/D,GAAG,wDAAwD;YACvD,MAAMgE,UAAU,IAAI7D,qBAAqB;gBACrCkC,aAAa1C,KAAKmB,IAAI,CAACiD,QAAQ;gBAC/BzB,aAAa3C,KAAKmB,IAAI,CAACiD,QAAQ;YACnC;YAEA,MAAME,SAASD,QAAQE,oBAAoB,CAAC;gBACxC;oBAAE/C,KAAK;oBAAGC,SAAS;oBAAUC,KAAK;oBAASC,KAAK;gBAAG;aACtD;YACDvB,OAAOkE,QAAQR,OAAO,CAAC,EAAE;QAC7B;QAEAzD,GAAG,sDAAsD;YACrD,0CAA0C;YAC1C,MAAMmE,aAAaxE,KAAKmB,IAAI,CAACuB,aAAa;YAC1C5C,GAAGiD,SAAS,CAACyB,YAAY;gBAAEjC,WAAW;YAAK;YAE3C,qDAAqD;YACrD,MAAMkC,WAAWzE,KAAKmB,IAAI,CAACuB,aAAa;YACxC5C,GAAGiD,SAAS,CAAC0B,UAAU;gBAAElC,WAAW;YAAK;YAEzC,MAAMmC,YAA2B;gBAC7B;oBACI9B,WAAW;oBACXsB,UAAUlE,KAAKmB,IAAI,CAACqD,YAAY;oBAChC3B,YAAY2B;oBACZG,aAAa;oBACbR,aAAa;gBACjB;aACH;YACDrD,oCAAoC8D,eAAe,CAACF;YAEpD,MAAMG,YAAY;gBACd;oBAAErD,KAAK;oBAAGC,SAAS;oBAAUC,KAAK;oBAAeC,KAAK;gBAAG;aAC5D;YAED,MAAM2C,SAASvC,QAAQwC,oBAAoB,CAACM;YAC5CzE,OAAOkE,QAAQV,YAAY,CAAC;YAC5BxD,OAAOkE,MAAM,CAAC,EAAE,CAACH,WAAW,EAAEW,IAAI,CAAC;YACnC,0DAA0D;YAC1D1E,OAAOU,qCAAqCiE,qBAAqB,CAAC;YAClE3E,OAAOU,qCAAqCkE,oBAAoB,CAAC;gBAACR;aAAW;QACjF;QAEAnE,GAAG,uDAAuD;YACtDP,GAAGiD,SAAS,CAACL,aAAa;gBAAEH,WAAW;YAAK;YAE5C,iEAAiE;YACjE,MAAM+B,SAASvC,QAAQwC,oBAAoB,CAAC;gBACxC;oBAAE/C,KAAK;oBAAGC,SAAS;oBAAUC,KAAK;oBAASC,KAAK;gBAAG;aACtD;YACDvB,OAAOkE,QAAQR,OAAO,CAAC,EAAE;YACzB1D,OAAOU,qCAAqCmE,GAAG,CAACC,gBAAgB;QACpE;QAEA7E,GAAG,6DAA6D;YAC5D,MAAMmE,aAAaxE,KAAKmB,IAAI,CAACuB,aAAa;YAC1C5C,GAAGiD,SAAS,CAACyB,YAAY;gBAAEjC,WAAW;YAAK;YAE3CzB,oCAAoC8D,eAAe,CAAC;gBAChD;oBAAEhC,WAAW;oBAAMsB,UAAUlE,KAAKmB,IAAI,CAACqD,YAAY;oBAAa3B,YAAY2B;oBAAYG,aAAa;oBAAeR,aAAa;gBAAG;aACvI;YAED,MAAMU,YAAY;gBACd;oBAAErD,KAAK;oBAAGC,SAAS;oBAAUC,KAAK;oBAAeC,KAAK;gBAAG;gBACzD;oBAAEH,KAAK;oBAAGC,SAAS;oBAAUC,KAAK;oBAAeC,KAAK;gBAAG;aAC5D;YAED,MAAM2C,SAASvC,QAAQwC,oBAAoB,CAACM;YAC5C,oDAAoD;YACpDzE,OAAOU,qCAAqCiE,qBAAqB,CAAC;YAClE3E,OAAOU,qCAAqCkE,oBAAoB,CAAC;gBAACR;aAAW;YAC7EpE,OAAOkE,QAAQV,YAAY,CAAC;QAChC;QAEAvD,GAAG,wCAAwC;YACvCP,GAAGiD,SAAS,CAACL,aAAa;gBAAEH,WAAW;YAAK;YAE5C,MAAM+B,SAASvC,QAAQwC,oBAAoB,CAAC;gBACxC;oBAAE/C,KAAK;oBAAGC,SAAS;oBAAUC,KAAK;oBAAIC,KAAK;gBAAG;aACjD;YACDvB,OAAOkE,QAAQR,OAAO,CAAC,EAAE;QAC7B;IACJ;IAEA3D,SAAS,sBAAsB;QAC3B,IAAIiE;QACJ,IAAIzB;QACJ,IAAID;QACJ,IAAIyC;QAKJjF,WAAW;YACPkE,SAASpD;YACT2B,cAAc3C,KAAKmB,IAAI,CAACiD,QAAQ;YAChC1B,cAAc1C,KAAKmB,IAAI,CAACiD,QAAQ;YAChCtE,GAAGiD,SAAS,CAACJ,aAAa;gBAAEJ,WAAW;YAAK;YAC5C4C,WAAWrD,oBAAoB,IAAItB,qBAAqB;gBAAEkC;gBAAaC;YAAY;QACvF;QAEA,MAAMyC,WAAW,CAAC5D,KAAaE,MAAM,eAAe,EAAEE,YAAmC,CAAA;gBACrFJ;gBAAKC,SAAS;gBAAUC;gBAAKC,KAAK;gBAAWC;YACjD,CAAA;QAEA,MAAMyD,eAAe,CAAC7D,KAAaoB,WAAmBlB,KAAayB;YAC/DrD,GAAGkD,aAAa,CACZhD,KAAKmB,IAAI,CAACwB,aAAa,GAAGnB,IAAI,KAAK,CAAC,GACpCyB,KAAKC,SAAS,CAAC;gBAAE1B;gBAAKoB;gBAAWlB;gBAAKyB;gBAAWE,MAAM;gBAAeC,YAAY;YAAM;QAEhG;QAEA,MAAMgC,aAAa,CAAC5D,KAAakB;YAC7B,MAAM2C,UAAU7D,IAAI8D,OAAO,CAAC,iBAAiB;YAC7C,MAAMC,UAAUzF,KAAKmB,IAAI,CAACuB,aAAa6C;YACvCzF,GAAGiD,SAAS,CAAC0C,SAAS;gBAAElD,WAAW;YAAK;YACxC,MAAM2B,WAAWlE,KAAKmB,IAAI,CAACsE,SAAS,GAAG7C,UAAU,MAAM,CAAC;YACxD9C,GAAGkD,aAAa,CAACkB,UAAUjB,KAAKC,SAAS,CAAC;gBAAEwC,MAAM;gBAAaC,WAAW,IAAI9D,OAAO+D,WAAW;YAAG;YACnG,OAAO1B;QACX;QAEA7D,GAAG,uFAAuF;YACtF,MAAMuB,YAAY,IAAIC;YACtB,MAAMgE,OAAOT,SAAS,MAAM,iBAAiBxD;YAC7CyD,aAAa,MAAM,eAAe,iBAAiBzD,UAAUwB,OAAO;YACpEkC,WAAW,iBAAiB;YAE5B,MAAM,EAAE3B,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;gBAACU;aAAK;YAE5CzF,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;YAC9BxD,OAAOuD,MAAM,CAAC,EAAE,CAACb,WAAW,CAACF,SAAS,EAAEkC,IAAI,CAAC;YAC7C1E,OAAOuD,MAAM,CAAC,EAAE,CAACb,WAAW,CAACqB,WAAW,EAAEW,IAAI,CAAC;YAC/C1E,OAAOuD,MAAM,CAAC,EAAE,CAACoC,OAAO,CAACvE,GAAG,EAAEsD,IAAI,CAAC;QACvC;QAEAzE,GAAG,8DAA8D;YAC7D,MAAMuB,YAAY,IAAIC;YACtB,MAAMgE,OAAOT,SAAS,MAAM,iBAAiBxD;YAC7CyD,aAAa,MAAM,uBAAuB,iBAAiBzD,UAAUwB,OAAO;YAC5E,wBAAwB;YAExB,MAAM,EAAEO,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;gBAACU;aAAK;YAE5CzF,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;YAC9BxD,OAAO0F,QAAQ,CAAC,EAAE,CAACtE,GAAG,EAAEsD,IAAI,CAAC;QACjC;QAEAzE,GAAG,uEAAuE;YACtE,MAAMuB,YAAY,IAAIC;YACtB,MAAMmE,YAAYpE,UAAUwB,OAAO,KAAK,QAAQ,qBAAqB;YACrE,MAAMyC,OAAOT,SAAS,MAAM,iBAAiBxD;YAC7CyD,aAAa,MAAM,iBAAiB,iBAAiBW;YACrDV,WAAW,iBAAiB;YAE5B,MAAM,EAAE3B,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;gBAACU;aAAK;YAE5CzF,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;QAClC;QAEAvD,GAAG,iEAAiE;YAChE,MAAMuB,YAAY,IAAIC;YACtB,MAAMoE,YAAYrE,UAAUwB,OAAO,KAAK,QAAQ,wCAAwC;YACxF,MAAMyC,OAAOT,SAAS,MAAM,iBAAiBxD;YAC7CyD,aAAa,MAAM,iBAAiB,iBAAiBY;YACrDX,WAAW,iBAAiB;YAE5B,MAAM,EAAE3B,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;gBAACU;aAAK;YAE5CzF,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;QAClC;QAEAvD,GAAG,4CAA4C;YAC3C,MAAMwF,OAAOT,SAAS,MAAM,iBAAiB,IAAIvD;YACjD,sBAAsB;YAEtB,MAAM,EAAE8B,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;gBAACU;aAAK;YAE5CzF,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;QAClC;QAEAvD,GAAG,0DAA0D;YACzD,MAAMwF,OAAOT,SAAS,MAAM,iBAAiB,IAAIvD;YACjD/B,GAAGkD,aAAa,CAAChD,KAAKmB,IAAI,CAACwB,aAAa,cAAc;YAEtDvC,OAAO;gBACH,MAAM,EAAEuD,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;oBAACU;iBAAK;gBAC5CzF,OAAOuD,QAAQC,YAAY,CAAC;gBAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;YAClC,GAAGqB,GAAG,CAACiB,OAAO;QAClB;QAEA7F,GAAG,uEAAuE;YACtE,MAAM8F,kBAAkBrE,oBAAoB,IAAItB,qBAAqB;gBACjEkC;gBACAC,aAAa3C,KAAKmB,IAAI,CAACiD,QAAQ;YACnC;YACA,MAAMS,YAAY;gBAACO,SAAS,MAAM,MAAM,IAAIvD;gBAASuD,SAAS,MAAM,MAAM,IAAIvD;aAAQ;YAEtF,MAAM,EAAE8B,MAAM,EAAEmC,QAAQ,EAAE,GAAGK,gBAAgBtB;YAE7CzE,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;QAClC;QAEAvD,GAAG,8EAA8E;YAC7E,MAAMuB,YAAY,IAAIC;YACtB,MAAMuE,QAAQhB,SAAS,MAAM,gBAAgBxD;YAC7C,MAAMyE,QAAQjB,SAAS,MAAM,gBAAgBxD;YAC7C,MAAM0E,QAAQlB,SAAS,MAAM,kBAAkBxD;YAE/CyD,aAAa,MAAM,eAAe,gBAAgBzD,UAAUwB,OAAO;YACnEkC,WAAW,gBAAgB;YAC3BD,aAAa,MAAM,iBAAiB,kBAAkBzD,UAAUwB,OAAO;YACvEkC,WAAW,kBAAkB;YAC7B,wBAAwB;YAExB,MAAM,EAAE3B,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;gBAACiB;gBAAOC;gBAAOC;aAAM;YAE3DlG,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;YAC9BxD,OAAOuD,OAAO4C,GAAG,CAAC,CAACC,IAAMA,EAAET,OAAO,CAACvE,GAAG,EAAEiF,IAAI,IAAI3C,OAAO,CAAC;gBAAC;gBAAM;aAAK;YACpE1D,OAAO0F,QAAQ,CAAC,EAAE,CAACtE,GAAG,EAAEsD,IAAI,CAAC;QACjC;QAEAzE,GAAG,iEAAiE;YAChE,MAAMwF,OAAOT,SAAS,MAAM,iBAAiBsB,YAAY,eAAe;YACxErB,aAAa,MAAM,mBAAmB,iBAAiBxD,KAAK8E,GAAG,KAAK;YACpErB,WAAW,iBAAiB;YAE5B,MAAM,EAAE3B,MAAM,EAAEmC,QAAQ,EAAE,GAAGX,SAAS;gBAACU;aAAK;YAE5C,2DAA2D;YAC3DzF,OAAOuD,QAAQC,YAAY,CAAC;YAC5BxD,OAAO0F,UAAUlC,YAAY,CAAC;QAClC;IACJ;IAEAzD,SAAS,iBAAiB;QACtB,IAAIiE;QACJ,IAAI1B;QACJ,IAAIkE;QAEJ1G,WAAW;YACPkE,SAASpD;YACT0B,cAAc1C,KAAKmB,IAAI,CAACiD,QAAQ;YAChCwC,SAAS1E,eAAe,IAAI1B,qBAAqB;gBAC7CkC;gBACAC,aAAa3C,KAAKmB,IAAI,CAACiD,QAAQ;YACnC;QACJ;QAEA/D,GAAG,+CAA+C;YAC9CD,OAAOwG,OAAO,mBAAmB9B,IAAI,CAAC9E,KAAKmB,IAAI,CAACuB,aAAa;QACjE;QAEArC,GAAG,kEAAkE;YACjED,OAAOwG,OAAO,0BAA0B9B,IAAI,CAAC9E,KAAKmB,IAAI,CAACuB,aAAa;QACxE;QAEArC,GAAG,iCAAiC;YAChCD,OAAOwG,OAAO,4BAA4B9B,IAAI,CAAC9E,KAAKmB,IAAI,CAACuB,aAAa;QAC1E;QAEArC,GAAG,mEAAmE;YAClE,qDAAqD;YACrD,yDAAyD;YACzDD,OAAOwG,OAAO,WAAW9B,IAAI,CAAC8B,OAAO;YACrCxG,OAAOwG,OAAO,WAAW9B,IAAI,CAAC8B,OAAO;QACzC;QAEAvG,GAAG,sEAAsE;YACrE,MAAMqB,MAAM;YACZ,MAAMmF,cAAc7G,KAAKmB,IAAI,CAACuB,aAAa;YAC3C5C,GAAGiD,SAAS,CAAC8D,aAAa;gBAAEtE,WAAW;YAAK;YAC5C,MAAMO,cAAc9C,KAAKmB,IAAI,CAAC0F,aAAa;YAC3C/G,GAAGkD,aAAa,CAACF,aAAa;YAE9B1C,OAAOwG,OAAOlF,MAAMoD,IAAI,CAAC+B;YACzBzG,OAAON,GAAGgH,UAAU,CAAC9G,KAAKmB,IAAI,CAACyF,OAAOlF,MAAM,8BAA8BoD,IAAI,CAAC;QACnF;IACJ;AACJ"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
describe('PiCliProbe', ()=>{
|
|
3
|
+
it('validates documented JSON mode and session capabilities', async ()=>{
|
|
4
|
+
const api = await import('../../../../index.js');
|
|
5
|
+
expect(api).toHaveProperty('PiCliProbe');
|
|
6
|
+
const exec = vi.fn().mockResolvedValueOnce({
|
|
7
|
+
stdout: 'pi 0.52.8',
|
|
8
|
+
stderr: ''
|
|
9
|
+
}).mockResolvedValueOnce({
|
|
10
|
+
stdout: '--mode json\n--session-id <uuid>\n--session <path|id>',
|
|
11
|
+
stderr: ''
|
|
12
|
+
});
|
|
13
|
+
const Probe = api.PiCliProbe;
|
|
14
|
+
await expect(new Probe({
|
|
15
|
+
executable: 'fake-pi',
|
|
16
|
+
exec
|
|
17
|
+
}).validate()).resolves.toEqual({
|
|
18
|
+
executable: 'fake-pi',
|
|
19
|
+
version: 'pi 0.52.8'
|
|
20
|
+
});
|
|
21
|
+
expect(exec.mock.calls).toEqual([
|
|
22
|
+
[
|
|
23
|
+
'fake-pi',
|
|
24
|
+
[
|
|
25
|
+
'--version'
|
|
26
|
+
]
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
'fake-pi',
|
|
30
|
+
[
|
|
31
|
+
'--help'
|
|
32
|
+
]
|
|
33
|
+
]
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
it('rejects unsupported and unavailable CLIs with sanitized errors', async ()=>{
|
|
37
|
+
const api = await import('../../../../index.js');
|
|
38
|
+
const Probe = api.PiCliProbe;
|
|
39
|
+
await expect(new Probe({
|
|
40
|
+
exec: vi.fn().mockResolvedValueOnce({
|
|
41
|
+
stdout: '',
|
|
42
|
+
stderr: ''
|
|
43
|
+
}).mockResolvedValueOnce({
|
|
44
|
+
stdout: '--print only',
|
|
45
|
+
stderr: ''
|
|
46
|
+
})
|
|
47
|
+
}).validate()).rejects.toMatchObject({
|
|
48
|
+
code: 'PI_CLI_UNSUPPORTED'
|
|
49
|
+
});
|
|
50
|
+
const unavailable = new Probe({
|
|
51
|
+
exec: vi.fn().mockRejectedValue(new Error(`bad\0${'x'.repeat(1000)}`))
|
|
52
|
+
});
|
|
53
|
+
const error = await unavailable.validate().catch((value)=>value);
|
|
54
|
+
expect(error.code).toBe('PI_CLI_UNAVAILABLE');
|
|
55
|
+
expect(error.message).not.toContain('\0');
|
|
56
|
+
expect(error.message.length).toBeLessThan(600);
|
|
57
|
+
});
|
|
58
|
+
it('reports an empty version as unknown', async ()=>{
|
|
59
|
+
const api = await import('../../../../index.js');
|
|
60
|
+
const Probe = api.PiCliProbe;
|
|
61
|
+
const exec = vi.fn().mockResolvedValueOnce({
|
|
62
|
+
stdout: ' \n',
|
|
63
|
+
stderr: ''
|
|
64
|
+
}).mockResolvedValueOnce({
|
|
65
|
+
stdout: '--mode json --session-id --session',
|
|
66
|
+
stderr: ''
|
|
67
|
+
});
|
|
68
|
+
await expect(new Probe({
|
|
69
|
+
exec
|
|
70
|
+
}).validate()).resolves.toMatchObject({
|
|
71
|
+
version: 'unknown'
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=PiCliProbe.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/__tests__/providers/pi/durable/PiCliProbe.test.ts"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest';\n\ndescribe('PiCliProbe', () => {\n it('validates documented JSON mode and session capabilities', async () => {\n const api = await import('../../../../index.js') as Record<string, unknown>;\n expect(api).toHaveProperty('PiCliProbe');\n const exec = vi.fn()\n .mockResolvedValueOnce({ stdout: 'pi 0.52.8', stderr: '' })\n .mockResolvedValueOnce({ stdout: '--mode json\\n--session-id <uuid>\\n--session <path|id>', stderr: '' });\n const Probe = api.PiCliProbe as new (options: unknown) => any;\n await expect(new Probe({ executable: 'fake-pi', exec }).validate()).resolves.toEqual({\n executable: 'fake-pi', version: 'pi 0.52.8',\n });\n expect(exec.mock.calls).toEqual([['fake-pi', ['--version']], ['fake-pi', ['--help']]]);\n });\n\n it('rejects unsupported and unavailable CLIs with sanitized errors', async () => {\n const api = await import('../../../../index.js') as Record<string, unknown>;\n const Probe = api.PiCliProbe as new (options: unknown) => any;\n await expect(new Probe({ exec: vi.fn()\n .mockResolvedValueOnce({ stdout: '', stderr: '' })\n .mockResolvedValueOnce({ stdout: '--print only', stderr: '' }) }).validate())\n .rejects.toMatchObject({ code: 'PI_CLI_UNSUPPORTED' });\n const unavailable = new Probe({ exec: vi.fn().mockRejectedValue(new Error(`bad\\0${'x'.repeat(1000)}`)) });\n const error = await unavailable.validate().catch((value: Error & { code: string }) => value);\n expect(error.code).toBe('PI_CLI_UNAVAILABLE');\n expect(error.message).not.toContain('\\0');\n expect(error.message.length).toBeLessThan(600);\n });\n\n it('reports an empty version as unknown', async () => {\n const api = await import('../../../../index.js') as Record<string, unknown>;\n const Probe = api.PiCliProbe as new (options: unknown) => any;\n const exec = vi.fn().mockResolvedValueOnce({ stdout: ' \\n', stderr: '' })\n .mockResolvedValueOnce({ stdout: '--mode json --session-id --session', stderr: '' });\n await expect(new Probe({ exec }).validate()).resolves.toMatchObject({ version: 'unknown' });\n });\n});\n"],"names":["describe","expect","it","vi","api","toHaveProperty","exec","fn","mockResolvedValueOnce","stdout","stderr","Probe","PiCliProbe","executable","validate","resolves","toEqual","version","mock","calls","rejects","toMatchObject","code","unavailable","mockRejectedValue","Error","repeat","error","catch","value","toBe","message","not","toContain","length","toBeLessThan"],"mappings":"AAAA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAElDH,SAAS,cAAc;IACnBE,GAAG,2DAA2D;QAC1D,MAAME,MAAM,MAAM,MAAM,CAAC;QACzBH,OAAOG,KAAKC,cAAc,CAAC;QAC3B,MAAMC,OAAOH,GAAGI,EAAE,GACbC,qBAAqB,CAAC;YAAEC,QAAQ;YAAaC,QAAQ;QAAG,GACxDF,qBAAqB,CAAC;YAAEC,QAAQ;YAAyDC,QAAQ;QAAG;QACzG,MAAMC,QAAQP,IAAIQ,UAAU;QAC5B,MAAMX,OAAO,IAAIU,MAAM;YAAEE,YAAY;YAAWP;QAAK,GAAGQ,QAAQ,IAAIC,QAAQ,CAACC,OAAO,CAAC;YACjFH,YAAY;YAAWI,SAAS;QACpC;QACAhB,OAAOK,KAAKY,IAAI,CAACC,KAAK,EAAEH,OAAO,CAAC;YAAC;gBAAC;gBAAW;oBAAC;iBAAY;aAAC;YAAE;gBAAC;gBAAW;oBAAC;iBAAS;aAAC;SAAC;IACzF;IAEAd,GAAG,kEAAkE;QACjE,MAAME,MAAM,MAAM,MAAM,CAAC;QACzB,MAAMO,QAAQP,IAAIQ,UAAU;QAC5B,MAAMX,OAAO,IAAIU,MAAM;YAAEL,MAAMH,GAAGI,EAAE,GAC/BC,qBAAqB,CAAC;gBAAEC,QAAQ;gBAAIC,QAAQ;YAAG,GAC/CF,qBAAqB,CAAC;gBAAEC,QAAQ;gBAAgBC,QAAQ;YAAG;QAAG,GAAGI,QAAQ,IACzEM,OAAO,CAACC,aAAa,CAAC;YAAEC,MAAM;QAAqB;QACxD,MAAMC,cAAc,IAAIZ,MAAM;YAAEL,MAAMH,GAAGI,EAAE,GAAGiB,iBAAiB,CAAC,IAAIC,MAAM,CAAC,KAAK,EAAE,IAAIC,MAAM,CAAC,OAAO;QAAG;QACvG,MAAMC,QAAQ,MAAMJ,YAAYT,QAAQ,GAAGc,KAAK,CAAC,CAACC,QAAoCA;QACtF5B,OAAO0B,MAAML,IAAI,EAAEQ,IAAI,CAAC;QACxB7B,OAAO0B,MAAMI,OAAO,EAAEC,GAAG,CAACC,SAAS,CAAC;QACpChC,OAAO0B,MAAMI,OAAO,CAACG,MAAM,EAAEC,YAAY,CAAC;IAC9C;IAEAjC,GAAG,uCAAuC;QACtC,MAAME,MAAM,MAAM,MAAM,CAAC;QACzB,MAAMO,QAAQP,IAAIQ,UAAU;QAC5B,MAAMN,OAAOH,GAAGI,EAAE,GAAGC,qBAAqB,CAAC;YAAEC,QAAQ;YAAOC,QAAQ;QAAG,GAClEF,qBAAqB,CAAC;YAAEC,QAAQ;YAAsCC,QAAQ;QAAG;QACtF,MAAMT,OAAO,IAAIU,MAAM;YAAEL;QAAK,GAAGQ,QAAQ,IAAIC,QAAQ,CAACM,aAAa,CAAC;YAAEJ,SAAS;QAAU;IAC7F;AACJ"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { DurableAgentRepository } from '../../../../durable/DurableAgentRepository.js';
|
|
6
|
+
const roots = [];
|
|
7
|
+
afterEach(()=>{
|
|
8
|
+
for (const root of roots.splice(0))fs.rmSync(root, {
|
|
9
|
+
recursive: true,
|
|
10
|
+
force: true
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('Pi durable-agent repository sessions', ()=>{
|
|
14
|
+
it('assigns a non-null session at creation and rejects deferred binding', async ()=>{
|
|
15
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-durable-repository-'));
|
|
16
|
+
roots.push(root);
|
|
17
|
+
const cwd = path.join(root, 'project');
|
|
18
|
+
fs.mkdirSync(cwd);
|
|
19
|
+
const repository = new DurableAgentRepository({
|
|
20
|
+
dbPath: path.join(root, 'agents.db'),
|
|
21
|
+
processInspector: {
|
|
22
|
+
getIdentity: (pid)=>({
|
|
23
|
+
pid,
|
|
24
|
+
startedAt: 'owner-start'
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const agent = await repository.create({
|
|
29
|
+
name: 'reviewer',
|
|
30
|
+
cwd,
|
|
31
|
+
provider: 'pi'
|
|
32
|
+
});
|
|
33
|
+
expect(agent).toMatchObject({
|
|
34
|
+
provider: 'pi',
|
|
35
|
+
sessionHealth: 'uninitialized'
|
|
36
|
+
});
|
|
37
|
+
expect(agent.providerSessionId).toMatch(/^[0-9a-f-]{36}$/);
|
|
38
|
+
const run = await repository.acquireRun(agent.id);
|
|
39
|
+
await expect(repository.bindProviderSession(agent.id, run.token, crypto.randomUUID())).rejects.toThrow('Only Codex durable sessions can be bound after creation.');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=PiDurableAgentRepository.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { afterEach, describe, expect, it } from 'vitest';\nimport { DurableAgentRepository } from '../../../../durable/DurableAgentRepository.js';\n\nconst roots: string[] = [];\n\nafterEach(() => {\n for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });\n});\n\ndescribe('Pi durable-agent repository sessions', () => {\n it('assigns a non-null session at creation and rejects deferred binding', async () => {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-durable-repository-'));\n roots.push(root);\n const cwd = path.join(root, 'project');\n fs.mkdirSync(cwd);\n const repository = new DurableAgentRepository({\n dbPath: path.join(root, 'agents.db'),\n processInspector: { getIdentity: (pid) => ({ pid, startedAt: 'owner-start' }) },\n });\n\n const agent = await repository.create({ name: 'reviewer', cwd, provider: 'pi' });\n expect(agent).toMatchObject({ provider: 'pi', sessionHealth: 'uninitialized' });\n expect(agent.providerSessionId).toMatch(/^[0-9a-f-]{36}$/);\n\n const run = await repository.acquireRun(agent.id);\n await expect(repository.bindProviderSession(agent.id, run.token, crypto.randomUUID()))\n .rejects.toThrow('Only Codex durable sessions can be bound after creation.');\n });\n});\n"],"names":["fs","os","path","afterEach","describe","expect","it","DurableAgentRepository","roots","root","splice","rmSync","recursive","force","mkdtempSync","join","tmpdir","push","cwd","mkdirSync","repository","dbPath","processInspector","getIdentity","pid","startedAt","agent","create","name","provider","toMatchObject","sessionHealth","providerSessionId","toMatch","run","acquireRun","id","bindProviderSession","token","crypto","randomUUID","rejects","toThrow"],"mappings":"AAAA,OAAOA,QAAQ,UAAU;AACzB,OAAOC,QAAQ,UAAU;AACzB,OAAOC,UAAU,YAAY;AAC7B,SAASC,SAAS,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AACzD,SAASC,sBAAsB,QAAQ,gDAAgD;AAEvF,MAAMC,QAAkB,EAAE;AAE1BL,UAAU;IACN,KAAK,MAAMM,QAAQD,MAAME,MAAM,CAAC,GAAIV,GAAGW,MAAM,CAACF,MAAM;QAAEG,WAAW;QAAMC,OAAO;IAAK;AACvF;AAEAT,SAAS,wCAAwC;IAC7CE,GAAG,uEAAuE;QACtE,MAAMG,OAAOT,GAAGc,WAAW,CAACZ,KAAKa,IAAI,CAACd,GAAGe,MAAM,IAAI;QACnDR,MAAMS,IAAI,CAACR;QACX,MAAMS,MAAMhB,KAAKa,IAAI,CAACN,MAAM;QAC5BT,GAAGmB,SAAS,CAACD;QACb,MAAME,aAAa,IAAIb,uBAAuB;YAC1Cc,QAAQnB,KAAKa,IAAI,CAACN,MAAM;YACxBa,kBAAkB;gBAAEC,aAAa,CAACC,MAAS,CAAA;wBAAEA;wBAAKC,WAAW;oBAAc,CAAA;YAAG;QAClF;QAEA,MAAMC,QAAQ,MAAMN,WAAWO,MAAM,CAAC;YAAEC,MAAM;YAAYV;YAAKW,UAAU;QAAK;QAC9ExB,OAAOqB,OAAOI,aAAa,CAAC;YAAED,UAAU;YAAME,eAAe;QAAgB;QAC7E1B,OAAOqB,MAAMM,iBAAiB,EAAEC,OAAO,CAAC;QAExC,MAAMC,MAAM,MAAMd,WAAWe,UAAU,CAACT,MAAMU,EAAE;QAChD,MAAM/B,OAAOe,WAAWiB,mBAAmB,CAACX,MAAMU,EAAE,EAAEF,IAAII,KAAK,EAAEC,OAAOC,UAAU,KAC7EC,OAAO,CAACC,OAAO,CAAC;IACzB;AACJ"}
|