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.
@@ -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
- beforeEach(() => {
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('createSession with command configuration', () => {
83
- it('should create session with default command when no args configured', async () => {
84
- // Setup mock configuration
85
- vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
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.createSession('/test/worktree');
92
- // Verify spawn was called with correct arguments
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 create session with configured arguments', async () => {
103
- // Setup mock configuration with args
104
- vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
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', '--model', 'opus'],
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.createSession('/test/worktree');
112
- // Verify spawn was called with configured arguments
113
- expect(spawn).toHaveBeenCalledWith('claude', ['--resume', '--model', 'opus'], expect.objectContaining({
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
- // Session creation verified by spawn being called
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 configuration with fallback
120
- vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
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.createSession('/test/worktree');
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 configuration with fallback
179
- vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
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.createSession('/test/worktree');
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 return existing session if already created', async () => {
196
- // Setup mock configuration
197
- vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
198
- command: 'claude',
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 twice
203
- const session1 = await sessionManager.createSession('/test/worktree');
204
- const session2 = await sessionManager.createSession('/test/worktree');
205
- // Should return the same session
206
- expect(session1).toBe(session2);
207
- // Spawn should only be called once
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 with fallback args', async () => {
211
- // Setup mock configuration with fallback
212
- vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
213
- command: 'nonexistent-command',
214
- args: ['--flag1'],
215
- fallbackArgs: ['--flag2'],
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 always throw error
292
+ // Mock spawn to throw error
218
293
  vi.mocked(spawn).mockImplementation(() => {
219
- throw new Error('Command not found');
294
+ throw new Error('spawn failed');
220
295
  });
221
- // Expect createSession to throw the original error
222
- await expect(sessionManager.createSession('/test/worktree')).rejects.toThrow('Command not found');
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.getCommandConfig).mockReturnValue({
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.createSession('/test/worktree');
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.getCommandConfig).mockReturnValue({
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.createSession('/test/worktree');
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('createSession with presets', () => {
263
- it('should use default preset when no preset ID specified', async () => {
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: ['--preset-arg'],
368
+ args: ['--resume'],
270
369
  });
271
370
  // Setup spawn mock
272
371
  vi.mocked(spawn).mockReturnValue(mockPty);
273
- // Create session with preset
274
- await sessionManager.createSessionWithPreset('/test/worktree');
275
- // Verify spawn was called with preset config
276
- expect(spawn).toHaveBeenCalledWith('claude', ['--preset-arg'], {
277
- name: 'xterm-color',
278
- cols: expect.any(Number),
279
- rows: expect.any(Number),
280
- cwd: '/test/worktree',
281
- env: process.env,
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
- await sessionManager.createSessionWithPreset('/test/worktree', '2');
297
- // Verify getPresetById was called with correct ID
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
- // Verify spawn was called with preset config
300
- expect(spawn).toHaveBeenCalledWith('claude', ['--resume', '--dev'], {
301
- name: 'xterm-color',
302
- cols: expect.any(Number),
303
- rows: expect.any(Number),
304
- cwd: '/test/worktree',
305
- env: process.env,
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 fall back to default preset if specified preset not found', async () => {
309
- // Setup mocks
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 non-existent preset
319
- await sessionManager.createSessionWithPreset('/test/worktree', 'invalid');
320
- // Verify fallback to default preset
321
- expect(configurationManager.getDefaultPreset).toHaveBeenCalled();
322
- expect(spawn).toHaveBeenCalledWith('claude', [], expect.any(Object));
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 try fallback args with preset if main command fails', async () => {
325
- // Setup mock preset with fallback
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: ['--bad-flag'],
331
- fallbackArgs: ['--good-flag'],
471
+ args: [],
332
472
  });
333
- // Mock spawn to fail first, succeed second
334
- let callCount = 0;
335
- vi.mocked(spawn).mockImplementation(() => {
336
- callCount++;
337
- if (callCount === 1) {
338
- throw new Error('Command failed');
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
- return mockPty;
481
+ if (callback && typeof callback === 'function') {
482
+ callback(null, 'Container started', '');
483
+ }
484
+ return {};
341
485
  });
342
- // Create session
343
- await sessionManager.createSessionWithPreset('/test/worktree');
344
- // Verify both attempts were made
345
- expect(spawn).toHaveBeenCalledTimes(2);
346
- expect(spawn).toHaveBeenNthCalledWith(1, 'claude', ['--bad-flag'], expect.any(Object));
347
- expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--good-flag'], expect.any(Object));
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 maintain backward compatibility with createSession', async () => {
350
- // Setup legacy config
351
- vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
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: ['--legacy'],
562
+ args: ['-m', 'claude-3-opus'],
354
563
  });
355
- // Setup spawn mock
356
- vi.mocked(spawn).mockReturnValue(mockPty);
357
- // Create session using legacy method
358
- await sessionManager.createSession('/test/worktree');
359
- // Verify legacy method still works
360
- expect(spawn).toHaveBeenCalledWith('claude', ['--legacy'], expect.any(Object));
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
  });