ccmanager 1.1.1 → 1.3.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/README.md +30 -0
- package/dist/cli.d.ts +8 -1
- package/dist/cli.js +31 -4
- package/dist/components/App.d.ts +5 -1
- package/dist/components/App.js +18 -7
- package/dist/components/Menu.d.ts +2 -0
- package/dist/components/Menu.js +13 -2
- package/dist/components/NewWorktree.d.ts +1 -1
- package/dist/components/NewWorktree.js +34 -2
- package/dist/integration-tests/devcontainer.integration.test.d.ts +1 -0
- package/dist/integration-tests/devcontainer.integration.test.js +101 -0
- package/dist/services/sessionManager.d.ts +5 -2
- package/dist/services/sessionManager.js +59 -46
- package/dist/services/sessionManager.test.js +358 -142
- package/dist/services/worktreeService.d.ts +3 -1
- package/dist/services/worktreeService.js +61 -2
- package/dist/services/worktreeService.test.js +165 -0
- package/dist/types/index.d.ts +5 -1
- package/package.json +1 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { SessionManager } from './sessionManager.js';
|
|
3
|
-
import { configurationManager } from './configurationManager.js';
|
|
4
2
|
import { spawn } from 'node-pty';
|
|
5
3
|
import { EventEmitter } from 'events';
|
|
4
|
+
import { exec } from 'child_process';
|
|
6
5
|
// Mock node-pty
|
|
7
6
|
vi.mock('node-pty');
|
|
7
|
+
// Mock child_process
|
|
8
|
+
vi.mock('child_process', () => ({
|
|
9
|
+
exec: vi.fn(),
|
|
10
|
+
}));
|
|
8
11
|
// Mock configuration manager
|
|
9
12
|
vi.mock('./configurationManager.js', () => ({
|
|
10
13
|
configurationManager: {
|
|
@@ -28,6 +31,10 @@ vi.mock('@xterm/headless', () => ({
|
|
|
28
31
|
})),
|
|
29
32
|
},
|
|
30
33
|
}));
|
|
34
|
+
// Mock worktreeService
|
|
35
|
+
vi.mock('./worktreeService.js', () => ({
|
|
36
|
+
WorktreeService: vi.fn(),
|
|
37
|
+
}));
|
|
31
38
|
// Create a mock IPty class
|
|
32
39
|
class MockPty extends EventEmitter {
|
|
33
40
|
constructor() {
|
|
@@ -71,53 +78,146 @@ class MockPty extends EventEmitter {
|
|
|
71
78
|
describe('SessionManager', () => {
|
|
72
79
|
let sessionManager;
|
|
73
80
|
let mockPty;
|
|
74
|
-
|
|
81
|
+
let SessionManager;
|
|
82
|
+
let configurationManager;
|
|
83
|
+
beforeEach(async () => {
|
|
75
84
|
vi.clearAllMocks();
|
|
85
|
+
// Dynamically import after mocks are set up
|
|
86
|
+
const sessionManagerModule = await import('./sessionManager.js');
|
|
87
|
+
const configManagerModule = await import('./configurationManager.js');
|
|
88
|
+
SessionManager = sessionManagerModule.SessionManager;
|
|
89
|
+
configurationManager = configManagerModule.configurationManager;
|
|
76
90
|
sessionManager = new SessionManager();
|
|
77
91
|
mockPty = new MockPty();
|
|
78
92
|
});
|
|
79
93
|
afterEach(() => {
|
|
80
94
|
sessionManager.destroy();
|
|
81
95
|
});
|
|
82
|
-
describe('
|
|
83
|
-
it('should
|
|
84
|
-
// Setup mock
|
|
85
|
-
vi.mocked(configurationManager.
|
|
96
|
+
describe('createSessionWithPreset', () => {
|
|
97
|
+
it('should use default preset when no preset ID specified', async () => {
|
|
98
|
+
// Setup mock preset
|
|
99
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
100
|
+
id: '1',
|
|
101
|
+
name: 'Main',
|
|
86
102
|
command: 'claude',
|
|
103
|
+
args: ['--preset-arg'],
|
|
87
104
|
});
|
|
88
105
|
// Setup spawn mock
|
|
89
106
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
90
|
-
// Create session
|
|
91
|
-
await sessionManager.
|
|
92
|
-
// Verify spawn was called with
|
|
93
|
-
expect(spawn).toHaveBeenCalledWith('claude', [], {
|
|
107
|
+
// Create session with preset
|
|
108
|
+
await sessionManager.createSessionWithPreset('/test/worktree');
|
|
109
|
+
// Verify spawn was called with preset config
|
|
110
|
+
expect(spawn).toHaveBeenCalledWith('claude', ['--preset-arg'], {
|
|
94
111
|
name: 'xterm-color',
|
|
95
112
|
cols: expect.any(Number),
|
|
96
113
|
rows: expect.any(Number),
|
|
97
114
|
cwd: '/test/worktree',
|
|
98
115
|
env: process.env,
|
|
99
116
|
});
|
|
100
|
-
// Session creation verified by spawn being called
|
|
101
117
|
});
|
|
102
|
-
it('should
|
|
103
|
-
// Setup mock
|
|
104
|
-
vi.mocked(configurationManager.
|
|
118
|
+
it('should use specific preset when ID provided', async () => {
|
|
119
|
+
// Setup mock preset
|
|
120
|
+
vi.mocked(configurationManager.getPresetById).mockReturnValue({
|
|
121
|
+
id: '2',
|
|
122
|
+
name: 'Development',
|
|
105
123
|
command: 'claude',
|
|
106
|
-
args: ['--resume', '--
|
|
124
|
+
args: ['--resume', '--dev'],
|
|
125
|
+
fallbackArgs: ['--no-mcp'],
|
|
107
126
|
});
|
|
108
127
|
// Setup spawn mock
|
|
109
128
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
110
|
-
// Create session
|
|
111
|
-
await sessionManager.
|
|
112
|
-
// Verify
|
|
113
|
-
expect(
|
|
129
|
+
// Create session with specific preset
|
|
130
|
+
await sessionManager.createSessionWithPreset('/test/worktree', '2');
|
|
131
|
+
// Verify getPresetById was called with correct ID
|
|
132
|
+
expect(configurationManager.getPresetById).toHaveBeenCalledWith('2');
|
|
133
|
+
// Verify spawn was called with preset config
|
|
134
|
+
expect(spawn).toHaveBeenCalledWith('claude', ['--resume', '--dev'], {
|
|
135
|
+
name: 'xterm-color',
|
|
136
|
+
cols: expect.any(Number),
|
|
137
|
+
rows: expect.any(Number),
|
|
114
138
|
cwd: '/test/worktree',
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
env: process.env,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
it('should fall back to default preset if specified preset not found', async () => {
|
|
143
|
+
// Setup mocks
|
|
144
|
+
vi.mocked(configurationManager.getPresetById).mockReturnValue(undefined);
|
|
145
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
146
|
+
id: '1',
|
|
147
|
+
name: 'Main',
|
|
148
|
+
command: 'claude',
|
|
149
|
+
});
|
|
150
|
+
// Setup spawn mock
|
|
151
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
152
|
+
// Create session with non-existent preset
|
|
153
|
+
await sessionManager.createSessionWithPreset('/test/worktree', 'invalid');
|
|
154
|
+
// Verify fallback to default preset
|
|
155
|
+
expect(configurationManager.getDefaultPreset).toHaveBeenCalled();
|
|
156
|
+
expect(spawn).toHaveBeenCalledWith('claude', [], expect.any(Object));
|
|
157
|
+
});
|
|
158
|
+
it('should try fallback args with preset if main command fails', async () => {
|
|
159
|
+
// Setup mock preset with fallback
|
|
160
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
161
|
+
id: '1',
|
|
162
|
+
name: 'Main',
|
|
163
|
+
command: 'claude',
|
|
164
|
+
args: ['--bad-flag'],
|
|
165
|
+
fallbackArgs: ['--good-flag'],
|
|
166
|
+
});
|
|
167
|
+
// Mock spawn to fail first, succeed second
|
|
168
|
+
let callCount = 0;
|
|
169
|
+
vi.mocked(spawn).mockImplementation(() => {
|
|
170
|
+
callCount++;
|
|
171
|
+
if (callCount === 1) {
|
|
172
|
+
throw new Error('Command failed');
|
|
173
|
+
}
|
|
174
|
+
return mockPty;
|
|
175
|
+
});
|
|
176
|
+
// Create session
|
|
177
|
+
await sessionManager.createSessionWithPreset('/test/worktree');
|
|
178
|
+
// Verify both attempts were made
|
|
179
|
+
expect(spawn).toHaveBeenCalledTimes(2);
|
|
180
|
+
expect(spawn).toHaveBeenNthCalledWith(1, 'claude', ['--bad-flag'], expect.any(Object));
|
|
181
|
+
expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--good-flag'], expect.any(Object));
|
|
182
|
+
});
|
|
183
|
+
it('should return existing session if already created', async () => {
|
|
184
|
+
// Setup mock preset
|
|
185
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
186
|
+
id: '1',
|
|
187
|
+
name: 'Main',
|
|
188
|
+
command: 'claude',
|
|
189
|
+
});
|
|
190
|
+
// Setup spawn mock
|
|
191
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
192
|
+
// Create session twice
|
|
193
|
+
const session1 = await sessionManager.createSessionWithPreset('/test/worktree');
|
|
194
|
+
const session2 = await sessionManager.createSessionWithPreset('/test/worktree');
|
|
195
|
+
// Should return the same session
|
|
196
|
+
expect(session1).toBe(session2);
|
|
197
|
+
// Spawn should only be called once
|
|
198
|
+
expect(spawn).toHaveBeenCalledTimes(1);
|
|
199
|
+
});
|
|
200
|
+
it('should throw error when spawn fails with fallback args', async () => {
|
|
201
|
+
// Setup mock preset with fallback
|
|
202
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
203
|
+
id: '1',
|
|
204
|
+
name: 'Main',
|
|
205
|
+
command: 'nonexistent-command',
|
|
206
|
+
args: ['--flag1'],
|
|
207
|
+
fallbackArgs: ['--flag2'],
|
|
208
|
+
});
|
|
209
|
+
// Mock spawn to always throw error
|
|
210
|
+
vi.mocked(spawn).mockImplementation(() => {
|
|
211
|
+
throw new Error('Command not found');
|
|
212
|
+
});
|
|
213
|
+
// Expect createSessionWithPreset to throw the original error
|
|
214
|
+
await expect(sessionManager.createSessionWithPreset('/test/worktree')).rejects.toThrow('Command not found');
|
|
117
215
|
});
|
|
118
216
|
it('should use fallback args when main command exits with code 1', async () => {
|
|
119
|
-
// Setup mock
|
|
120
|
-
vi.mocked(configurationManager.
|
|
217
|
+
// Setup mock preset with fallback
|
|
218
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
219
|
+
id: '1',
|
|
220
|
+
name: 'Main',
|
|
121
221
|
command: 'claude',
|
|
122
222
|
args: ['--invalid-flag'],
|
|
123
223
|
fallbackArgs: ['--resume'],
|
|
@@ -130,7 +230,7 @@ describe('SessionManager', () => {
|
|
|
130
230
|
.mockReturnValueOnce(firstMockPty)
|
|
131
231
|
.mockReturnValueOnce(secondMockPty);
|
|
132
232
|
// Create session
|
|
133
|
-
const session = await sessionManager.
|
|
233
|
+
const session = await sessionManager.createSessionWithPreset('/test/worktree');
|
|
134
234
|
// Verify initial spawn
|
|
135
235
|
expect(spawn).toHaveBeenCalledTimes(1);
|
|
136
236
|
expect(spawn).toHaveBeenCalledWith('claude', ['--invalid-flag'], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
@@ -145,38 +245,11 @@ describe('SessionManager', () => {
|
|
|
145
245
|
expect(session.process).toBe(secondMockPty);
|
|
146
246
|
expect(session.isPrimaryCommand).toBe(false);
|
|
147
247
|
});
|
|
148
|
-
it('should throw error when spawn fails and no fallback configured', async () => {
|
|
149
|
-
// Setup mock configuration without fallback
|
|
150
|
-
vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
|
|
151
|
-
command: 'claude',
|
|
152
|
-
args: ['--invalid-flag'],
|
|
153
|
-
});
|
|
154
|
-
// Mock spawn to throw error
|
|
155
|
-
vi.mocked(spawn).mockImplementation(() => {
|
|
156
|
-
throw new Error('spawn failed');
|
|
157
|
-
});
|
|
158
|
-
// Expect createSession to throw
|
|
159
|
-
await expect(sessionManager.createSession('/test/worktree')).rejects.toThrow('spawn failed');
|
|
160
|
-
});
|
|
161
|
-
it('should handle custom command configuration', async () => {
|
|
162
|
-
// Setup mock configuration with custom command
|
|
163
|
-
vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
|
|
164
|
-
command: 'my-custom-claude',
|
|
165
|
-
args: ['--config', '/path/to/config'],
|
|
166
|
-
});
|
|
167
|
-
// Setup spawn mock
|
|
168
|
-
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
169
|
-
// Create session
|
|
170
|
-
await sessionManager.createSession('/test/worktree');
|
|
171
|
-
// Verify spawn was called with custom command
|
|
172
|
-
expect(spawn).toHaveBeenCalledWith('my-custom-claude', ['--config', '/path/to/config'], expect.objectContaining({
|
|
173
|
-
cwd: '/test/worktree',
|
|
174
|
-
}));
|
|
175
|
-
// Session creation verified by spawn being called
|
|
176
|
-
});
|
|
177
248
|
it('should not use fallback if main command succeeds', async () => {
|
|
178
|
-
// Setup mock
|
|
179
|
-
vi.mocked(configurationManager.
|
|
249
|
+
// Setup mock preset with fallback
|
|
250
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
251
|
+
id: '1',
|
|
252
|
+
name: 'Main',
|
|
180
253
|
command: 'claude',
|
|
181
254
|
args: ['--resume'],
|
|
182
255
|
fallbackArgs: ['--other-flag'],
|
|
@@ -184,53 +257,57 @@ describe('SessionManager', () => {
|
|
|
184
257
|
// Setup spawn mock - process doesn't exit early
|
|
185
258
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
186
259
|
// Create session
|
|
187
|
-
await sessionManager.
|
|
260
|
+
await sessionManager.createSessionWithPreset('/test/worktree');
|
|
188
261
|
// Wait a bit to ensure no early exit
|
|
189
262
|
await new Promise(resolve => setTimeout(resolve, 600));
|
|
190
263
|
// Verify only one spawn attempt
|
|
191
264
|
expect(spawn).toHaveBeenCalledTimes(1);
|
|
192
265
|
expect(spawn).toHaveBeenCalledWith('claude', ['--resume'], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
193
|
-
// Session creation verified by spawn being called
|
|
194
266
|
});
|
|
195
|
-
it('should
|
|
196
|
-
// Setup mock
|
|
197
|
-
vi.mocked(configurationManager.
|
|
198
|
-
|
|
267
|
+
it('should handle custom command configuration', async () => {
|
|
268
|
+
// Setup mock preset with custom command
|
|
269
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
270
|
+
id: '1',
|
|
271
|
+
name: 'Main',
|
|
272
|
+
command: 'my-custom-claude',
|
|
273
|
+
args: ['--config', '/path/to/config'],
|
|
199
274
|
});
|
|
200
275
|
// Setup spawn mock
|
|
201
276
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
202
|
-
// Create session
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
expect(spawn).toHaveBeenCalledTimes(1);
|
|
277
|
+
// Create session
|
|
278
|
+
await sessionManager.createSessionWithPreset('/test/worktree');
|
|
279
|
+
// Verify spawn was called with custom command
|
|
280
|
+
expect(spawn).toHaveBeenCalledWith('my-custom-claude', ['--config', '/path/to/config'], expect.objectContaining({
|
|
281
|
+
cwd: '/test/worktree',
|
|
282
|
+
}));
|
|
209
283
|
});
|
|
210
|
-
it('should throw error when spawn fails
|
|
211
|
-
// Setup mock
|
|
212
|
-
vi.mocked(configurationManager.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
284
|
+
it('should throw error when spawn fails and no fallback configured', async () => {
|
|
285
|
+
// Setup mock preset without fallback
|
|
286
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
287
|
+
id: '1',
|
|
288
|
+
name: 'Main',
|
|
289
|
+
command: 'claude',
|
|
290
|
+
args: ['--invalid-flag'],
|
|
216
291
|
});
|
|
217
|
-
// Mock spawn to
|
|
292
|
+
// Mock spawn to throw error
|
|
218
293
|
vi.mocked(spawn).mockImplementation(() => {
|
|
219
|
-
throw new Error('
|
|
294
|
+
throw new Error('spawn failed');
|
|
220
295
|
});
|
|
221
|
-
// Expect
|
|
222
|
-
await expect(sessionManager.
|
|
296
|
+
// Expect createSessionWithPreset to throw
|
|
297
|
+
await expect(sessionManager.createSessionWithPreset('/test/worktree')).rejects.toThrow('spawn failed');
|
|
223
298
|
});
|
|
224
299
|
});
|
|
225
300
|
describe('session lifecycle', () => {
|
|
226
301
|
it('should destroy session and clean up resources', async () => {
|
|
227
302
|
// Setup
|
|
228
|
-
vi.mocked(configurationManager.
|
|
303
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
304
|
+
id: '1',
|
|
305
|
+
name: 'Main',
|
|
229
306
|
command: 'claude',
|
|
230
307
|
});
|
|
231
308
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
232
309
|
// Create and destroy session
|
|
233
|
-
await sessionManager.
|
|
310
|
+
await sessionManager.createSessionWithPreset('/test/worktree');
|
|
234
311
|
sessionManager.destroySession('/test/worktree');
|
|
235
312
|
// Verify cleanup
|
|
236
313
|
expect(mockPty.kill).toHaveBeenCalled();
|
|
@@ -238,7 +315,9 @@ describe('SessionManager', () => {
|
|
|
238
315
|
});
|
|
239
316
|
it('should handle session exit event', async () => {
|
|
240
317
|
// Setup
|
|
241
|
-
vi.mocked(configurationManager.
|
|
318
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
319
|
+
id: '1',
|
|
320
|
+
name: 'Main',
|
|
242
321
|
command: 'claude',
|
|
243
322
|
});
|
|
244
323
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
@@ -248,7 +327,7 @@ describe('SessionManager', () => {
|
|
|
248
327
|
exitedSession = session;
|
|
249
328
|
});
|
|
250
329
|
// Create session
|
|
251
|
-
const createdSession = await sessionManager.
|
|
330
|
+
const createdSession = await sessionManager.createSessionWithPreset('/test/worktree');
|
|
252
331
|
// Simulate process exit after successful creation
|
|
253
332
|
setTimeout(() => {
|
|
254
333
|
mockPty.emit('exit', { exitCode: 0 });
|
|
@@ -259,27 +338,46 @@ describe('SessionManager', () => {
|
|
|
259
338
|
expect(sessionManager.getSession('/test/worktree')).toBeUndefined();
|
|
260
339
|
});
|
|
261
340
|
});
|
|
262
|
-
describe('
|
|
263
|
-
|
|
341
|
+
describe('createSessionWithDevcontainer', () => {
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
// Reset shouldFail flag
|
|
344
|
+
const mockExec = vi.mocked(exec);
|
|
345
|
+
mockExec.shouldFail = false;
|
|
346
|
+
// Setup exec mock to work with promisify
|
|
347
|
+
mockExec.mockImplementation(((...args) => {
|
|
348
|
+
const [command, , callback] = args;
|
|
349
|
+
if (callback) {
|
|
350
|
+
// Handle callback style
|
|
351
|
+
if (command.includes('devcontainer up')) {
|
|
352
|
+
if (mockExec.shouldFail) {
|
|
353
|
+
callback(new Error('Container startup failed'));
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
callback(null, '', '');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}));
|
|
361
|
+
});
|
|
362
|
+
it('should execute devcontainer up command before creating session', async () => {
|
|
264
363
|
// Setup mock preset
|
|
265
364
|
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
266
365
|
id: '1',
|
|
267
366
|
name: 'Main',
|
|
268
367
|
command: 'claude',
|
|
269
|
-
args: ['--
|
|
368
|
+
args: ['--resume'],
|
|
270
369
|
});
|
|
271
370
|
// Setup spawn mock
|
|
272
371
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
273
|
-
// Create session with
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
372
|
+
// Create session with devcontainer
|
|
373
|
+
const devcontainerConfig = {
|
|
374
|
+
upCommand: 'devcontainer up --workspace-folder .',
|
|
375
|
+
execCommand: 'devcontainer exec --workspace-folder .',
|
|
376
|
+
};
|
|
377
|
+
await sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig);
|
|
378
|
+
// Verify spawn was called correctly which proves devcontainer up succeeded
|
|
379
|
+
// Verify spawn was called with devcontainer exec
|
|
380
|
+
expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude', '--resume'], expect.objectContaining({ cwd: '/test/worktree' }));
|
|
283
381
|
});
|
|
284
382
|
it('should use specific preset when ID provided', async () => {
|
|
285
383
|
// Setup mock preset
|
|
@@ -288,76 +386,194 @@ describe('SessionManager', () => {
|
|
|
288
386
|
name: 'Development',
|
|
289
387
|
command: 'claude',
|
|
290
388
|
args: ['--resume', '--dev'],
|
|
291
|
-
fallbackArgs: ['--no-mcp'],
|
|
292
389
|
});
|
|
293
390
|
// Setup spawn mock
|
|
294
391
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
295
|
-
// Create session with specific preset
|
|
296
|
-
|
|
297
|
-
|
|
392
|
+
// Create session with devcontainer and specific preset
|
|
393
|
+
const devcontainerConfig = {
|
|
394
|
+
upCommand: 'devcontainer up',
|
|
395
|
+
execCommand: 'devcontainer exec',
|
|
396
|
+
};
|
|
397
|
+
await sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig, '2');
|
|
398
|
+
// Verify correct preset was used
|
|
298
399
|
expect(configurationManager.getPresetById).toHaveBeenCalledWith('2');
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
400
|
+
expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--', 'claude', '--resume', '--dev'], expect.any(Object));
|
|
401
|
+
});
|
|
402
|
+
it('should throw error when devcontainer up fails', async () => {
|
|
403
|
+
// Setup exec to fail
|
|
404
|
+
const mockExec = vi.mocked(exec);
|
|
405
|
+
mockExec.shouldFail = true;
|
|
406
|
+
// Create session with devcontainer
|
|
407
|
+
const devcontainerConfig = {
|
|
408
|
+
upCommand: 'devcontainer up',
|
|
409
|
+
execCommand: 'devcontainer exec',
|
|
410
|
+
};
|
|
411
|
+
await expect(sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig)).rejects.toThrow('Failed to start devcontainer: Container startup failed');
|
|
412
|
+
});
|
|
413
|
+
it('should return existing session if already created', async () => {
|
|
414
|
+
// Setup mock preset
|
|
415
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
416
|
+
id: '1',
|
|
417
|
+
name: 'Main',
|
|
418
|
+
command: 'claude',
|
|
306
419
|
});
|
|
420
|
+
// Setup spawn mock
|
|
421
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
422
|
+
const devcontainerConfig = {
|
|
423
|
+
upCommand: 'devcontainer up',
|
|
424
|
+
execCommand: 'devcontainer exec',
|
|
425
|
+
};
|
|
426
|
+
// Create session twice
|
|
427
|
+
const session1 = await sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig);
|
|
428
|
+
const session2 = await sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig);
|
|
429
|
+
// Should return the same session
|
|
430
|
+
expect(session1).toBe(session2);
|
|
431
|
+
// spawn should only be called once
|
|
432
|
+
expect(spawn).toHaveBeenCalledTimes(1);
|
|
307
433
|
});
|
|
308
|
-
it('should
|
|
309
|
-
// Setup
|
|
310
|
-
vi.mocked(configurationManager.getPresetById).mockReturnValue(undefined);
|
|
434
|
+
it('should handle complex exec commands with multiple arguments', async () => {
|
|
435
|
+
// Setup mock preset
|
|
311
436
|
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
312
437
|
id: '1',
|
|
313
438
|
name: 'Main',
|
|
314
439
|
command: 'claude',
|
|
440
|
+
args: ['--model', 'opus'],
|
|
315
441
|
});
|
|
316
442
|
// Setup spawn mock
|
|
317
443
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
318
|
-
// Create session with
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
444
|
+
// Create session with complex exec command
|
|
445
|
+
const devcontainerConfig = {
|
|
446
|
+
upCommand: 'devcontainer up --workspace-folder . --log-level debug',
|
|
447
|
+
execCommand: 'devcontainer exec --workspace-folder . --container-name mycontainer',
|
|
448
|
+
};
|
|
449
|
+
await sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig);
|
|
450
|
+
// Verify spawn was called with properly parsed exec command
|
|
451
|
+
expect(spawn).toHaveBeenCalledWith('devcontainer', [
|
|
452
|
+
'exec',
|
|
453
|
+
'--workspace-folder',
|
|
454
|
+
'.',
|
|
455
|
+
'--container-name',
|
|
456
|
+
'mycontainer',
|
|
457
|
+
'--',
|
|
458
|
+
'claude',
|
|
459
|
+
'--model',
|
|
460
|
+
'opus',
|
|
461
|
+
], expect.any(Object));
|
|
323
462
|
});
|
|
324
|
-
it('should
|
|
325
|
-
//
|
|
463
|
+
it('should spawn process with devcontainer exec command', async () => {
|
|
464
|
+
// Create a new session manager and reset mocks
|
|
465
|
+
vi.clearAllMocks();
|
|
466
|
+
sessionManager = new SessionManager();
|
|
326
467
|
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
327
468
|
id: '1',
|
|
328
469
|
name: 'Main',
|
|
329
470
|
command: 'claude',
|
|
330
|
-
args: [
|
|
331
|
-
fallbackArgs: ['--good-flag'],
|
|
471
|
+
args: [],
|
|
332
472
|
});
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
vi.mocked(
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
|
|
473
|
+
// Setup spawn mock
|
|
474
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
475
|
+
const mockExec = vi.mocked(exec);
|
|
476
|
+
mockExec.mockImplementation((cmd, options, callback) => {
|
|
477
|
+
if (typeof options === 'function') {
|
|
478
|
+
callback = options;
|
|
479
|
+
options = undefined;
|
|
339
480
|
}
|
|
340
|
-
|
|
481
|
+
if (callback && typeof callback === 'function') {
|
|
482
|
+
callback(null, 'Container started', '');
|
|
483
|
+
}
|
|
484
|
+
return {};
|
|
341
485
|
});
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
expect(spawn).
|
|
486
|
+
await sessionManager.createSessionWithDevcontainer('/test/worktree2', {
|
|
487
|
+
upCommand: 'devcontainer up --workspace-folder .',
|
|
488
|
+
execCommand: 'devcontainer exec --workspace-folder .',
|
|
489
|
+
});
|
|
490
|
+
// Should spawn with devcontainer exec command
|
|
491
|
+
expect(spawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude'], expect.objectContaining({
|
|
492
|
+
cwd: '/test/worktree2',
|
|
493
|
+
}));
|
|
494
|
+
});
|
|
495
|
+
it('should use preset with devcontainer', async () => {
|
|
496
|
+
const mockExec = vi.mocked(exec);
|
|
497
|
+
mockExec.mockImplementation((cmd, options, callback) => {
|
|
498
|
+
if (typeof options === 'function') {
|
|
499
|
+
callback = options;
|
|
500
|
+
options = undefined;
|
|
501
|
+
}
|
|
502
|
+
if (callback && typeof callback === 'function') {
|
|
503
|
+
callback(null, 'Container started', '');
|
|
504
|
+
}
|
|
505
|
+
return {};
|
|
506
|
+
});
|
|
507
|
+
await sessionManager.createSessionWithDevcontainer('/test/worktree', {
|
|
508
|
+
upCommand: 'devcontainer up --workspace-folder .',
|
|
509
|
+
execCommand: 'devcontainer exec --workspace-folder .',
|
|
510
|
+
}, 'custom-preset');
|
|
511
|
+
// Should call createSessionWithPreset internally
|
|
512
|
+
const session = sessionManager.getSession('/test/worktree');
|
|
513
|
+
expect(session).toBeDefined();
|
|
514
|
+
expect(session?.devcontainerConfig).toEqual({
|
|
515
|
+
upCommand: 'devcontainer up --workspace-folder .',
|
|
516
|
+
execCommand: 'devcontainer exec --workspace-folder .',
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
it('should parse exec command and append preset command', async () => {
|
|
520
|
+
const mockExec = vi.mocked(exec);
|
|
521
|
+
mockExec.mockImplementation((cmd, options, callback) => {
|
|
522
|
+
if (typeof options === 'function') {
|
|
523
|
+
callback = options;
|
|
524
|
+
options = undefined;
|
|
525
|
+
}
|
|
526
|
+
if (callback && typeof callback === 'function') {
|
|
527
|
+
callback(null, 'Container started', '');
|
|
528
|
+
}
|
|
529
|
+
return {};
|
|
530
|
+
});
|
|
531
|
+
const config = {
|
|
532
|
+
upCommand: 'devcontainer up --workspace-folder /path/to/project',
|
|
533
|
+
execCommand: 'devcontainer exec --workspace-folder /path/to/project --user vscode',
|
|
534
|
+
};
|
|
535
|
+
await sessionManager.createSessionWithDevcontainer('/test/worktree', config);
|
|
536
|
+
expect(spawn).toHaveBeenCalledWith('devcontainer', [
|
|
537
|
+
'exec',
|
|
538
|
+
'--workspace-folder',
|
|
539
|
+
'/path/to/project',
|
|
540
|
+
'--user',
|
|
541
|
+
'vscode',
|
|
542
|
+
'--',
|
|
543
|
+
'claude',
|
|
544
|
+
], expect.any(Object));
|
|
348
545
|
});
|
|
349
|
-
it('should
|
|
350
|
-
|
|
351
|
-
|
|
546
|
+
it('should handle preset with args in devcontainer', async () => {
|
|
547
|
+
const mockExec = vi.mocked(exec);
|
|
548
|
+
mockExec.mockImplementation((cmd, options, callback) => {
|
|
549
|
+
if (typeof options === 'function') {
|
|
550
|
+
callback = options;
|
|
551
|
+
options = undefined;
|
|
552
|
+
}
|
|
553
|
+
if (callback && typeof callback === 'function') {
|
|
554
|
+
callback(null, 'Container started', '');
|
|
555
|
+
}
|
|
556
|
+
return {};
|
|
557
|
+
});
|
|
558
|
+
vi.mocked(configurationManager.getPresetById).mockReturnValue({
|
|
559
|
+
id: 'claude-with-args',
|
|
560
|
+
name: 'Claude with Args',
|
|
352
561
|
command: 'claude',
|
|
353
|
-
args: ['
|
|
562
|
+
args: ['-m', 'claude-3-opus'],
|
|
354
563
|
});
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
564
|
+
await sessionManager.createSessionWithDevcontainer('/test/worktree', {
|
|
565
|
+
upCommand: 'devcontainer up --workspace-folder .',
|
|
566
|
+
execCommand: 'devcontainer exec --workspace-folder .',
|
|
567
|
+
}, 'claude-with-args');
|
|
568
|
+
expect(spawn).toHaveBeenCalledWith('devcontainer', [
|
|
569
|
+
'exec',
|
|
570
|
+
'--workspace-folder',
|
|
571
|
+
'.',
|
|
572
|
+
'--',
|
|
573
|
+
'claude',
|
|
574
|
+
'-m',
|
|
575
|
+
'claude-3-opus',
|
|
576
|
+
], expect.any(Object));
|
|
361
577
|
});
|
|
362
578
|
});
|
|
363
579
|
});
|